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

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

625 statements  

1from __future__ import annotations 

2 

3import operator 

4from operator import ( 

5 le, 

6 lt, 

7) 

8from typing import ( 

9 TYPE_CHECKING, 

10 Any, 

11 Literal, 

12 Self, 

13 TypeAlias, 

14 overload, 

15) 

16import warnings 

17 

18import numpy as np 

19 

20from pandas._libs import lib 

21from pandas._libs.interval import ( 

22 VALID_CLOSED, 

23 Interval, 

24 IntervalMixin, 

25 intervals_to_interval_bounds, 

26) 

27from pandas._libs.missing import NA 

28from pandas._typing import ( 

29 ArrayLike, 

30 AxisInt, 

31 Dtype, 

32 IntervalClosedType, 

33 NpDtype, 

34 PositionalIndexer, 

35 ScalarIndexer, 

36 SequenceIndexer, 

37 SortKind, 

38 TimeArrayLike, 

39 npt, 

40) 

41from pandas.compat.numpy import function as nv 

42from pandas.errors import ( 

43 IntCastingNaNError, 

44 Pandas4Warning, 

45) 

46from pandas.util._decorators import set_module 

47from pandas.util._exceptions import find_stack_level 

48 

49from pandas.core.dtypes.cast import LossySetitemError 

50from pandas.core.dtypes.common import ( 

51 is_float_dtype, 

52 is_integer_dtype, 

53 is_list_like, 

54 is_object_dtype, 

55 is_scalar, 

56 is_string_dtype, 

57 needs_i8_conversion, 

58 pandas_dtype, 

59) 

60from pandas.core.dtypes.dtypes import ( 

61 CategoricalDtype, 

62 IntervalDtype, 

63) 

64from pandas.core.dtypes.generic import ( 

65 ABCDataFrame, 

66 ABCDatetimeIndex, 

67 ABCIntervalIndex, 

68 ABCPeriodIndex, 

69) 

70from pandas.core.dtypes.missing import ( 

71 is_valid_na_for_dtype, 

72 isna, 

73 notna, 

74) 

75 

76from pandas.core import ops 

77from pandas.core.algorithms import ( 

78 isin, 

79 take, 

80 unique, 

81) 

82from pandas.core.arrays import ArrowExtensionArray 

83from pandas.core.arrays.base import ( 

84 ExtensionArray, 

85) 

86from pandas.core.arrays.datetimes import DatetimeArray 

87from pandas.core.arrays.timedeltas import TimedeltaArray 

88import pandas.core.common as com 

89from pandas.core.construction import ( 

90 array as pd_array, 

91 ensure_wrapped_if_datetimelike, 

92 extract_array, 

93) 

94from pandas.core.indexers import ( 

95 check_array_indexer, 

96 getitem_returns_view, 

97) 

98from pandas.core.ops import ( 

99 invalid_comparison, 

100 unpack_zerodim_and_defer, 

101) 

102 

103if TYPE_CHECKING: 

104 from collections.abc import ( 

105 Callable, 

106 Iterator, 

107 Sequence, 

108 ) 

109 

110 from pandas import ( 

111 Index, 

112 ) 

113 

114 

115IntervalSide: TypeAlias = TimeArrayLike | np.ndarray 

116IntervalOrNA: TypeAlias = "Interval[Any] | float" 

117 

118# Fixed salts for the four VALID_CLOSED values, used in _hash_pandas_object so 

119# the result is deterministic across processes (unlike the builtin str hash). 

120_CLOSED_HASH_VALUES = {"left": 0, "right": 1, "both": 2, "neither": 3} 

121 

122 

123@set_module("pandas.arrays") 

124class IntervalArray(IntervalMixin, ExtensionArray): 

125 """ 

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

127 

128 An IntervalArray stores an array of Interval objects, where each Interval 

129 represents a contiguous span of values. All intervals in the array must 

130 have the same closure (left, right, both, or neither). 

131 

132 Parameters 

133 ---------- 

134 data : array-like (1-dimensional) 

135 Array-like (ndarray, :class:`DatetimeArray`, :class:`TimedeltaArray`) containing 

136 Interval objects from which to build the IntervalArray. 

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

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

139 neither. 

140 dtype : dtype or None, default None 

141 If None, dtype will be inferred. 

142 copy : bool, default False 

143 Copy the input data. 

144 verify_integrity : bool, default True 

145 Verify that the IntervalArray is valid. 

146 

147 Attributes 

148 ---------- 

149 left 

150 right 

151 closed 

152 mid 

153 length 

154 is_empty 

155 is_non_overlapping_monotonic 

156 

157 Methods 

158 ------- 

159 from_arrays 

160 from_tuples 

161 from_breaks 

162 contains 

163 overlaps 

164 set_closed 

165 to_tuples 

166 

167 See Also 

168 -------- 

169 Index : The base pandas Index type. 

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

171 interval_range : Function to create a fixed frequency IntervalIndex. 

172 cut : Bin values into discrete Intervals. 

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

174 

175 Notes 

176 ----- 

177 See the `user guide 

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

179 for more. 

180 

181 Examples 

182 -------- 

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

184 ``Interval`` objects: 

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

186 <IntervalArray> 

187 [(0, 1], (1, 5]] 

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

189 

190 It may also be constructed using one of the constructor 

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

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

193 """ 

194 

195 can_hold_na = True 

196 _na_value = _fill_value = np.nan 

197 

198 @property 

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

200 return 1 

201 

202 # To make mypy recognize the fields 

203 _left: IntervalSide 

204 _right: IntervalSide 

205 _dtype: IntervalDtype 

206 

207 # --------------------------------------------------------------------- 

208 # Constructors 

209 

210 def __new__( 

211 cls, 

212 data, 

213 closed: IntervalClosedType | None = None, 

214 dtype: Dtype | None = None, 

215 copy: bool = False, 

216 verify_integrity: bool = True, 

217 ) -> Self: 

218 data = extract_array(data, extract_numpy=True) 

219 

220 if isinstance(data, cls): 

221 left: IntervalSide = data._left 

222 right: IntervalSide = data._right 

223 closed = closed or data.closed 

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

225 else: 

226 # don't allow scalars 

227 if is_scalar(data): 

228 msg = ( 

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

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

231 ) 

232 raise TypeError(msg) 

233 

234 # might need to convert empty or purely na data 

235 data = _maybe_convert_platform_interval(data) 

236 left, right, infer_closed = intervals_to_interval_bounds( 

237 data, validate_closed=closed is None 

238 ) 

239 if left.dtype == object: 

240 left = lib.maybe_convert_objects(left) 

241 right = lib.maybe_convert_objects(right) 

242 closed = closed or infer_closed 

243 

244 left, right, dtype = cls._ensure_simple_new_inputs( 

245 left, 

246 right, 

247 closed=closed, 

248 copy=copy, 

249 dtype=dtype, 

250 ) 

251 

252 if verify_integrity: 

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

254 

255 return cls._simple_new( 

256 left, 

257 right, 

258 dtype=dtype, 

259 ) 

260 

261 @classmethod 

262 def _simple_new( 

263 cls, 

264 left: IntervalSide, 

265 right: IntervalSide, 

266 dtype: IntervalDtype, 

267 ) -> Self: 

268 result = IntervalMixin.__new__(cls) 

269 result._left = left 

270 result._right = right 

271 result._dtype = dtype 

272 

273 return result 

274 

275 @classmethod 

276 def _ensure_simple_new_inputs( 

277 cls, 

278 left, 

279 right, 

280 closed: IntervalClosedType | None = None, 

281 copy: bool = False, 

282 dtype: Dtype | None = None, 

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

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

285 from pandas.core.indexes.base import ensure_index 

286 

287 left = ensure_index(left, copy=copy) 

288 

289 right = ensure_index(right, copy=copy) 

290 

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

292 closed = dtype.closed 

293 

294 closed = closed or "right" 

295 

296 if dtype is not None: 

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

298 dtype = pandas_dtype(dtype) 

299 if isinstance(dtype, IntervalDtype): 

300 if dtype.subtype is not None: 

301 left = left.astype(dtype.subtype) 

302 right = right.astype(dtype.subtype) 

303 else: 

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

305 raise TypeError(msg) 

306 

307 if dtype.closed is None: 

308 # possibly loading an old pickle 

309 dtype = IntervalDtype(dtype.subtype, closed) 

310 elif closed != dtype.closed: 

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

312 

313 # coerce dtypes to match if needed 

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

315 right = right.astype(left.dtype) 

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

317 left = left.astype(right.dtype) 

318 

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

320 msg = ( 

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

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

323 ) 

324 raise ValueError(msg) 

325 if ( 

326 isinstance(left.dtype, CategoricalDtype) 

327 or is_string_dtype(left.dtype) 

328 or is_string_dtype(right.dtype) 

329 ): 

330 # GH 19016, GH 66518: reject unsupported right-side dtypes too. 

331 msg = ( 

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

333 "for IntervalArray" 

334 ) 

335 raise TypeError(msg) 

336 if isinstance(left, ABCPeriodIndex): 

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

338 raise ValueError(msg) 

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

340 msg = ( 

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

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

343 ) 

344 raise ValueError(msg) 

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

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

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

348 left = ensure_index(left_arr) 

349 right = ensure_index(right_arr) 

350 

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

352 left = ensure_wrapped_if_datetimelike(left) 

353 left = extract_array(left, extract_numpy=True) 

354 right = ensure_wrapped_if_datetimelike(right) 

355 right = extract_array(right, extract_numpy=True) 

356 

357 if isinstance(left, ArrowExtensionArray) or isinstance( 

358 right, ArrowExtensionArray 

359 ): 

360 pass 

361 else: 

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

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

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

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

366 if lbase is not None and lbase is rbase: 

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

368 right = right.copy() 

369 

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

371 

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

373 left_dtype = left.dtype 

374 right_dtype = right.dtype 

375 if ( 

376 left_dtype.kind in "iu" 

377 and right_dtype.kind in "iu" 

378 and left_dtype.kind != right_dtype.kind 

379 ): 

380 raise TypeError( 

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

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

383 ) 

384 return left, right, dtype 

385 

386 @classmethod 

387 def _from_sequence( 

388 cls, 

389 scalars, 

390 *, 

391 dtype: Dtype | None = None, 

392 copy: bool = False, 

393 ) -> Self: 

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

395 

396 @classmethod 

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

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

399 

400 @classmethod 

401 def from_breaks( 

402 cls, 

403 breaks, 

404 closed: IntervalClosedType | None = "right", 

405 copy: bool = False, 

406 dtype: Dtype | None = None, 

407 ) -> Self: 

408 """ 

409 Construct an IntervalArray from an array of splits. 

410 

411 This method creates intervals from consecutive pairs of break points, 

412 where each break point is the right edge of one interval and the left 

413 edge of the next. 

414 

415 Parameters 

416 ---------- 

417 breaks : array-like (1-dimensional) 

418 Left and right bounds for each interval. 

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

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

421 or neither. 

422 copy : bool, default False 

423 Copy the data. 

424 dtype : dtype or None, default None 

425 If None, dtype will be inferred. 

426 

427 Returns 

428 ------- 

429 IntervalArray 

430 

431 See Also 

432 -------- 

433 interval_range : Function to create a fixed frequency IntervalIndex. 

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

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

436 

437 Examples 

438 -------- 

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

440 <IntervalArray> 

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

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

443 """ 

444 

445 breaks = _maybe_convert_platform_interval(breaks) 

446 

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

448 

449 @classmethod 

450 def from_arrays( 

451 cls, 

452 left, 

453 right, 

454 closed: IntervalClosedType | None = "right", 

455 copy: bool = False, 

456 dtype: Dtype | None = None, 

457 ) -> Self: 

458 """ 

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

460 

461 This method creates an IntervalArray from two arrays of equal length, 

462 where the i-th interval spans from left[i] to right[i]. 

463 

464 Parameters 

465 ---------- 

466 left : array-like (1-dimensional) 

467 Left bounds for each interval. 

468 right : array-like (1-dimensional) 

469 Right bounds for each interval. 

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

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

472 or neither. 

473 copy : bool, default False 

474 Copy the data. 

475 dtype : dtype, optional 

476 If None, dtype will be inferred. 

477 

478 Returns 

479 ------- 

480 IntervalArray 

481 

482 Raises 

483 ------ 

484 ValueError 

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

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

487 in `right`. 

488 

489 See Also 

490 -------- 

491 interval_range : Function to create a fixed frequency IntervalIndex. 

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

493 splits. 

494 IntervalArray.from_tuples : Construct an IntervalArray from an 

495 array-like of tuples. 

496 

497 Notes 

498 ----- 

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

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

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

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

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

504 

505 Examples 

506 -------- 

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

508 <IntervalArray> 

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

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

511 """ 

512 left = _maybe_convert_platform_interval(left) 

513 right = _maybe_convert_platform_interval(right) 

514 

515 left, right, dtype = cls._ensure_simple_new_inputs( 

516 left, 

517 right, 

518 closed=closed, 

519 copy=copy, 

520 dtype=dtype, 

521 ) 

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

523 

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

525 

526 @classmethod 

527 def from_tuples( 

528 cls, 

529 data, 

530 closed: IntervalClosedType | None = "right", 

531 copy: bool = False, 

532 dtype: Dtype | None = None, 

533 ) -> Self: 

534 """ 

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

536 

537 This method creates an IntervalArray from a sequence of (left, right) 

538 tuples, where each tuple defines the bounds of an interval. 

539 

540 Parameters 

541 ---------- 

542 data : array-like (1-dimensional) 

543 Array of tuples. 

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

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

546 or neither. 

547 copy : bool, default False 

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

549 dtype : dtype or None, default None 

550 If None, dtype will be inferred. 

551 

552 Returns 

553 ------- 

554 IntervalArray 

555 

556 See Also 

557 -------- 

558 interval_range : Function to create a fixed frequency IntervalIndex. 

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

560 right array. 

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

562 splits. 

563 

564 Examples 

565 -------- 

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

567 <IntervalArray> 

568 [(0, 1], (1, 2]] 

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

570 """ 

571 if len(data): 

572 left, right = [], [] 

573 else: 

574 # ensure that empty data keeps input dtype 

575 left = right = data 

576 

577 for d in data: 

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

579 lhs = rhs = np.nan 

580 else: 

581 name = cls.__name__ 

582 try: 

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

584 lhs, rhs = d 

585 except ValueError as err: 

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

587 raise ValueError(msg) from err 

588 except TypeError as err: 

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

590 raise TypeError(msg) from err 

591 left.append(lhs) 

592 right.append(rhs) 

593 

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

595 

596 @classmethod 

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

598 """ 

599 Verify that the IntervalArray is valid. 

600 

601 Checks that 

602 

603 * dtype is correct 

604 * left and right match lengths 

605 * left and right have the same missing values 

606 * left is always below right 

607 """ 

608 if not isinstance(dtype, IntervalDtype): 

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

610 raise ValueError(msg) 

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

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

613 raise ValueError(msg) 

614 left_mask = notna(left) 

615 right_mask = notna(right) 

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

617 msg = ( 

618 "missing values must be missing in the same " 

619 "location both left and right sides" 

620 ) 

621 raise ValueError(msg) 

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

623 # GH#66807 point at the offending intervals 

624 invalid = left_mask.copy() 

625 invalid[left_mask] = left[left_mask] > right[left_mask] 

626 positions = np.flatnonzero(invalid) 

627 examples = ", ".join( 

628 f"{pos}: ({left[pos]}, {right[pos]})" for pos in positions[:5] 

629 ) 

630 if len(positions) > 5: 

631 examples += f", ... ({len(positions) - 5} more)" 

632 msg = ( 

633 "left side of interval must be <= right side; offending " 

634 f"intervals (position: interval): {examples}" 

635 ) 

636 raise ValueError(msg) 

637 

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

639 """ 

640 Return a new IntervalArray with the replacement attributes 

641 

642 Parameters 

643 ---------- 

644 left : Index 

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

646 right : Index 

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

648 """ 

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

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

651 

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

653 

654 # --------------------------------------------------------------------- 

655 # Descriptive 

656 

657 @property 

658 def dtype(self) -> IntervalDtype: 

659 return self._dtype 

660 

661 @property 

662 def nbytes(self) -> int: 

663 return self.left.nbytes + self.right.nbytes 

664 

665 @property 

666 def size(self) -> int: 

667 # Avoid materializing self.values 

668 return self.left.size 

669 

670 # --------------------------------------------------------------------- 

671 # EA Interface 

672 

673 def __iter__(self) -> Iterator[Any]: 

674 return iter(np.asarray(self)) 

675 

676 def __len__(self) -> int: 

677 return len(self._left) 

678 

679 @overload 

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

681 

682 @overload 

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

684 

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

686 key = check_array_indexer(self, key) 

687 left = self._left[key] 

688 right = self._right[key] 

689 

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

691 # scalar 

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

693 return self._fill_value 

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

695 if np.ndim(left) > 1: 

696 # GH#30588 multi-dimensional indexer disallowed 

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

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

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

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

701 # ndarray[Any, Any]]" 

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

703 if getitem_returns_view(self, key): 

704 result._readonly = self._readonly 

705 return result 

706 

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

708 if self._readonly: 

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

710 

711 key = check_array_indexer(self, key) 

712 value_left, value_right = self._validate_setitem_value(value) 

713 

714 self._left[key] = value_left 

715 self._right[key] = value_right 

716 

717 def _cmp_method(self, other, op): 

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

719 if is_list_like(other): 

720 if not isinstance( 

721 other, (list, np.ndarray, ExtensionArray) 

722 ) and not ops.has_castable_attr(other): 

723 warnings.warn( 

724 f"Operation with {type(other).__name__} is deprecated. " 

725 "In a future version these will be treated as scalar-like. " 

726 "To retain the old behavior, explicitly wrap in a Series " 

727 "instead.", 

728 Pandas4Warning, 

729 stacklevel=find_stack_level(), 

730 ) 

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

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

733 other = pd_array(other) 

734 elif not isinstance(other, Interval): 

735 # non-interval scalar -> no matches 

736 if other is NA: 

737 # GH#31882 

738 from pandas.core.arrays import BooleanArray 

739 

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

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

742 return BooleanArray(arr, mask) 

743 return invalid_comparison(self, other, op) 

744 

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

746 if isinstance(other, Interval): 

747 other_dtype = pandas_dtype("interval") 

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

749 other_dtype = other.dtype 

750 else: 

751 # for categorical defer to categories for dtype 

752 other_dtype = other.categories.dtype 

753 

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

755 if isinstance(other_dtype, IntervalDtype): 

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

757 return invalid_comparison(self, other, op) 

758 

759 other = other.categories._values.take( 

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

761 ) 

762 

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

764 if isinstance(other_dtype, IntervalDtype): 

765 if self.closed != other.closed: 

766 return invalid_comparison(self, other, op) 

767 elif not isinstance(other, Interval): 

768 other = type(self)(other) 

769 

770 if op is operator.eq: 

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

772 elif op is operator.ne: 

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

774 elif op is operator.gt: 

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

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

777 ) 

778 elif op is operator.ge: 

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

780 elif op is operator.lt: 

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

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

783 ) 

784 else: 

785 # operator.lt 

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

787 

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

789 if not is_object_dtype(other_dtype): 

790 return invalid_comparison(self, other, op) 

791 

792 # object dtype -> iteratively check for intervals 

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

794 for i, obj in enumerate(other): 

795 try: 

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

797 except TypeError: 

798 if obj is NA: 

799 # comparison with np.nan returns NA 

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

801 result = result.astype(object) 

802 result[i] = NA 

803 else: 

804 raise 

805 return result 

806 

807 @unpack_zerodim_and_defer("__eq__") 

808 def __eq__(self, other): 

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

810 

811 @unpack_zerodim_and_defer("__ne__") 

812 def __ne__(self, other): 

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

814 

815 @unpack_zerodim_and_defer("__gt__") 

816 def __gt__(self, other): 

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

818 

819 @unpack_zerodim_and_defer("__ge__") 

820 def __ge__(self, other): 

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

822 

823 @unpack_zerodim_and_defer("__lt__") 

824 def __lt__(self, other): 

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

826 

827 @unpack_zerodim_and_defer("__le__") 

828 def __le__(self, other): 

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

830 

831 def argsort( 

832 self, 

833 *, 

834 ascending: bool = True, 

835 kind: SortKind = "quicksort", 

836 na_position: str = "last", 

837 **kwargs, 

838 ) -> np.ndarray: 

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

840 

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

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

843 # IntervalTree.left_sorter 

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

845 

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

847 return super().argsort( 

848 ascending=ascending, kind=kind, na_position=na_position, **kwargs 

849 ) 

850 

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

852 nv.validate_minmax_axis(axis, self.ndim) 

853 

854 if not len(self): 

855 return self._na_value 

856 

857 mask = self.isna() 

858 if mask.any(): 

859 if not skipna: 

860 return self._na_value 

861 obj = self[~mask] 

862 else: 

863 obj = self 

864 

865 indexer = obj.argsort()[0] 

866 return obj[indexer] 

867 

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

869 nv.validate_minmax_axis(axis, self.ndim) 

870 

871 if not len(self): 

872 return self._na_value 

873 

874 mask = self.isna() 

875 if mask.any(): 

876 if not skipna: 

877 return self._na_value 

878 obj = self[~mask] 

879 else: 

880 obj = self 

881 

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

883 return obj[indexer] 

884 

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

886 """ 

887 Fill NA/NaN values using the specified method. 

888 

889 Parameters 

890 ---------- 

891 value : scalar, dict, Series 

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

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

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

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

896 limit : int, default None 

897 (Not implemented yet for IntervalArray) 

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

899 copy : bool, default True 

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

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

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

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

904 

905 Returns 

906 ------- 

907 filled : IntervalArray with NA/NaN filled 

908 """ 

909 if copy is False: 

910 raise NotImplementedError 

911 if isinstance(value, dict): 

912 raise TypeError( 

913 "ExtensionArray.fillna does not support filling with a dict. " 

914 "Use Series.fillna instead." 

915 ) 

916 if limit is not None: 

917 raise ValueError("limit must be None") 

918 

919 value_left, value_right = self._validate_setitem_value(value) 

920 

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

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

923 return self._shallow_copy(left, right) 

924 

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

926 """ 

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

928 

929 Parameters 

930 ---------- 

931 dtype : str or dtype 

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

933 

934 copy : bool, default True 

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

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

937 new dtype. 

938 

939 Returns 

940 ------- 

941 array : ExtensionArray or ndarray 

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

943 """ 

944 from pandas import Index 

945 

946 if dtype is not None: 

947 dtype = pandas_dtype(dtype) 

948 

949 if isinstance(dtype, IntervalDtype): 

950 if dtype == self.dtype: 

951 return self.copy() if copy else self 

952 

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

954 dtype.subtype 

955 ): 

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

957 msg = ( 

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

959 ) 

960 raise TypeError(msg) 

961 

962 # need to cast to different subtype 

963 try: 

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

965 # np.nan entries to int subtypes 

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

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

968 except IntCastingNaNError: 

969 # e.g test_subtype_integer 

970 raise 

971 except (TypeError, ValueError) as err: 

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

973 # and raises ValueError 

974 msg = ( 

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

976 ) 

977 raise TypeError(msg) from err 

978 return self._shallow_copy(new_left, new_right) 

979 else: 

980 try: 

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

982 except (TypeError, ValueError) as err: 

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

984 raise TypeError(msg) from err 

985 

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

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

988 return False 

989 

990 return bool( 

991 self.closed == other.closed 

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

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

994 ) 

995 

996 @classmethod 

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

998 """ 

999 Concatenate multiple IntervalArray 

1000 

1001 Parameters 

1002 ---------- 

1003 to_concat : sequence of IntervalArray 

1004 

1005 Returns 

1006 ------- 

1007 IntervalArray 

1008 """ 

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

1010 if len(closed_set) != 1: 

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

1012 closed = closed_set.pop() 

1013 

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

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

1016 

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

1018 

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

1020 

1021 def copy(self) -> Self: 

1022 """ 

1023 Return a copy of the array. 

1024 

1025 Returns 

1026 ------- 

1027 IntervalArray 

1028 """ 

1029 left = self._left.copy() 

1030 right = self._right.copy() 

1031 dtype = self.dtype 

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

1033 

1034 def _hash_pandas_object( 

1035 self, *, encoding: str, hash_key: str, categorize: bool 

1036 ) -> npt.NDArray[np.uint64]: 

1037 from pandas.core.util.hashing import ( 

1038 combine_hash_arrays, 

1039 hash_array, 

1040 ) 

1041 

1042 left_hash = hash_array( 

1043 self._left, encoding=encoding, hash_key=hash_key, categorize=categorize 

1044 ) 

1045 right_hash = hash_array( 

1046 self._right, encoding=encoding, hash_key=hash_key, categorize=categorize 

1047 ) 

1048 # Use a fixed mapping rather than hash(self.closed): the builtin str 

1049 # hash is salted per process (PYTHONHASHSEED), which would make the 

1050 # result nondeterministic across processes. GH#64605 

1051 closed_val = np.uint64(_CLOSED_HASH_VALUES[self.closed]) 

1052 closed_hash = hash_array( 

1053 np.full(len(self), closed_val, dtype=np.uint64), 

1054 encoding=encoding, 

1055 hash_key=hash_key, 

1056 categorize=False, 

1057 ) 

1058 return combine_hash_arrays( 

1059 iter([left_hash, right_hash, closed_hash]), num_items=3 

1060 ) 

1061 

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

1063 return isna(self._left) 

1064 

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

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

1067 return self.copy() 

1068 

1069 self._validate_scalar(fill_value) 

1070 

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

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

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

1074 # not other values like NaT 

1075 

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

1077 if isna(fill_value): 

1078 from pandas import Index 

1079 

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

1081 empty = IntervalArray.from_breaks( 

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

1083 ) 

1084 else: 

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

1086 

1087 if periods > 0: 

1088 a = empty 

1089 b = self[:-periods] 

1090 else: 

1091 a = self[abs(periods) :] 

1092 b = empty 

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

1094 

1095 def take( 

1096 self, 

1097 indices, 

1098 *, 

1099 allow_fill: bool = False, 

1100 fill_value=None, 

1101 axis=None, 

1102 **kwargs, 

1103 ) -> Self: 

1104 """ 

1105 Take elements from the IntervalArray. 

1106 

1107 Parameters 

1108 ---------- 

1109 indices : sequence of integers 

1110 Indices to be taken. 

1111 

1112 allow_fill : bool, default False 

1113 How to handle negative values in `indices`. 

1114 

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

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

1117 :func:`numpy.take`. 

1118 

1119 * True: negative values in `indices` indicate 

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

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

1122 

1123 fill_value : Interval or NA, optional 

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

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

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

1127 

1128 For many ExtensionArrays, there will be two representations of 

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

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

1131 and the implementation should handle translating that to the 

1132 physical version for processing the take if necessary. 

1133 

1134 axis : any, default None 

1135 Present for compat with IntervalIndex; does nothing. 

1136 

1137 Returns 

1138 ------- 

1139 IntervalArray 

1140 

1141 Raises 

1142 ------ 

1143 IndexError 

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

1145 ValueError 

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

1147 and `allow_fill` is True. 

1148 """ 

1149 nv.validate_take((), kwargs) 

1150 

1151 fill_left = fill_right = fill_value 

1152 if allow_fill: 

1153 fill_left, fill_right = self._validate_scalar(fill_value) 

1154 

1155 left_take = take( 

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

1157 ) 

1158 right_take = take( 

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

1160 ) 

1161 

1162 return self._shallow_copy(left_take, right_take) 

1163 

1164 def _validate_listlike(self, value): 

1165 # list-like of intervals 

1166 try: 

1167 array = IntervalArray(value) 

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

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

1170 except TypeError as err: 

1171 # wrong type: not interval or NA 

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

1173 raise TypeError(msg) from err 

1174 

1175 try: 

1176 self.left._validate_fill_value(value_left) 

1177 except (LossySetitemError, TypeError) as err: 

1178 msg = ( 

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

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

1181 ) 

1182 raise TypeError(msg) from err 

1183 

1184 return value_left, value_right 

1185 

1186 def _validate_scalar(self, value): 

1187 if isinstance(value, Interval): 

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

1189 left, right = value.left, value.right 

1190 self.left._validate_fill_value(left) 

1191 self.left._validate_fill_value(right) 

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

1193 # GH#18295 

1194 left = right = self.left._na_value 

1195 else: 

1196 raise TypeError( 

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

1198 ) 

1199 return left, right 

1200 

1201 def _validate_setitem_value(self, value): 

1202 if is_list_like(value): 

1203 return self._validate_listlike(value) 

1204 

1205 left, right = self._validate_scalar(value) 

1206 

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

1208 if is_integer_dtype(self.dtype.subtype): 

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

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

1211 # non-NA un-holdable value. 

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

1213 

1214 return left, right 

1215 

1216 # --------------------------------------------------------------------- 

1217 # Rendering Methods 

1218 

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

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

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

1222 return str 

1223 

1224 # --------------------------------------------------------------------- 

1225 # Vectorized Interval Properties/Attributes 

1226 

1227 @property 

1228 def left(self) -> Index: 

1229 """ 

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

1231 

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

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

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

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

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

1237 object containing the midpoint for each interval. 

1238 

1239 See Also 

1240 -------- 

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

1242 the IntervalArray as an Index. 

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

1244 IntervalArray as an Index. 

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

1246 the value. 

1247 

1248 Examples 

1249 -------- 

1250 

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

1252 >>> interv_arr 

1253 <IntervalArray> 

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

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

1256 >>> interv_arr.left 

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

1258 """ 

1259 from pandas import Index 

1260 

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

1262 

1263 @property 

1264 def right(self) -> Index: 

1265 """ 

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

1267 

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

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

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

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

1272 boundaries of data segments. 

1273 

1274 See Also 

1275 -------- 

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

1277 the IntervalArray as an Index. 

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

1279 IntervalArray as an Index. 

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

1281 the value. 

1282 

1283 Examples 

1284 -------- 

1285 

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

1287 >>> interv_arr 

1288 <IntervalArray> 

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

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

1291 >>> interv_arr.right 

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

1293 """ 

1294 from pandas import Index 

1295 

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

1297 

1298 @property 

1299 def length(self) -> Index: 

1300 """ 

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

1302 

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

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

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

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

1307 

1308 See Also 

1309 -------- 

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

1311 the IntervalArray as an Index. 

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

1313 the IntervalArray as an Index. 

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

1315 IntervalArray as an Index. 

1316 

1317 Examples 

1318 -------- 

1319 

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

1321 >>> interv_arr 

1322 <IntervalArray> 

1323 [(0, 1], (1, 5]] 

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

1325 >>> interv_arr.length 

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

1327 """ 

1328 return self.right - self.left 

1329 

1330 @property 

1331 def mid(self) -> Index: 

1332 """ 

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

1334 

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

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

1337 containing the midpoint for each interval. 

1338 

1339 See Also 

1340 -------- 

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

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

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

1344 

1345 Examples 

1346 -------- 

1347 

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

1349 >>> interv_arr 

1350 <IntervalArray> 

1351 [(0, 1], (1, 5]] 

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

1353 >>> interv_arr.mid 

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

1355 """ 

1356 try: 

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

1358 except TypeError: 

1359 # datetime safe version 

1360 return self.left + 0.5 * self.length 

1361 

1362 def overlaps(self, other): 

1363 """ 

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

1365 

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

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

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

1369 overlap. 

1370 

1371 Parameters 

1372 ---------- 

1373 other : IntervalArray 

1374 Interval to check against for an overlap. 

1375 

1376 Returns 

1377 ------- 

1378 ndarray 

1379 Boolean array positionally indicating where an overlap occurs. 

1380 

1381 See Also 

1382 -------- 

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

1384 

1385 Examples 

1386 -------- 

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

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

1389 >>> intervals 

1390 <IntervalArray> 

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

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

1393 

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

1395 array([ True, True, False]) 

1396 

1397 Intervals that share closed endpoints overlap: 

1398 

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

1400 array([ True, True, True]) 

1401 

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

1403 

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

1405 array([False, True, False]) 

1406 

1407 Empty intervals do not overlap with anything: 

1408 

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

1410 array([False, False, False]) 

1411 """ 

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

1413 raise NotImplementedError 

1414 if not isinstance(other, Interval): 

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

1416 raise TypeError(msg) 

1417 

1418 # an empty interval contains no points, so it cannot share one 

1419 if other.is_empty: 

1420 return np.zeros(len(self), dtype=bool) 

1421 

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

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

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

1425 

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

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

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

1429 overlaps = op1(self.left, other.right) & op2(other.left, self.right) 

1430 return overlaps & ~self.is_empty 

1431 

1432 # --------------------------------------------------------------------- 

1433 

1434 @property 

1435 def closed(self) -> IntervalClosedType: 

1436 """ 

1437 String describing the inclusive side the intervals. 

1438 

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

1440 

1441 See Also 

1442 -------- 

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

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

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

1446 

1447 Examples 

1448 -------- 

1449 

1450 For arrays: 

1451 

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

1453 >>> interv_arr 

1454 <IntervalArray> 

1455 [(0, 1], (1, 5]] 

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

1457 >>> interv_arr.closed 

1458 'right' 

1459 

1460 For Interval Index: 

1461 

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

1463 >>> interv_idx 

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

1465 >>> interv_idx.closed 

1466 'right' 

1467 """ 

1468 return self.dtype.closed 

1469 

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

1471 """ 

1472 Return an identical IntervalArray closed on the specified side. 

1473 

1474 This method creates a new IntervalArray with the same bounds but with 

1475 a different closure specification. 

1476 

1477 Parameters 

1478 ---------- 

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

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

1481 or neither. 

1482 

1483 Returns 

1484 ------- 

1485 IntervalArray 

1486 A new IntervalArray with the specified side closures. 

1487 

1488 See Also 

1489 -------- 

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

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

1492 

1493 Examples 

1494 -------- 

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

1496 >>> index 

1497 <IntervalArray> 

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

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

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

1501 <IntervalArray> 

1502 [[0, 1], [1, 2], [2, 3]] 

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

1504 """ 

1505 if closed not in VALID_CLOSED: 

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

1507 raise ValueError(msg) 

1508 

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

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

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

1512 

1513 @property 

1514 def is_non_overlapping_monotonic(self) -> bool: 

1515 """ 

1516 Return a boolean whether the IntervalArray/IntervalIndex\ 

1517 is non-overlapping and monotonic. 

1518 

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

1520 either monotonic increasing or monotonic decreasing. 

1521 

1522 See Also 

1523 -------- 

1524 overlaps : Check if two IntervalIndex objects overlap. 

1525 

1526 Examples 

1527 -------- 

1528 For arrays: 

1529 

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

1531 >>> interv_arr 

1532 <IntervalArray> 

1533 [(0, 1], (1, 5]] 

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

1535 >>> interv_arr.is_non_overlapping_monotonic 

1536 True 

1537 

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

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

1540 ... ) 

1541 >>> interv_arr 

1542 <IntervalArray> 

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

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

1545 >>> interv_arr.is_non_overlapping_monotonic 

1546 False 

1547 

1548 For Interval Index: 

1549 

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

1551 >>> interv_idx 

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

1553 >>> interv_idx.is_non_overlapping_monotonic 

1554 True 

1555 

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

1557 >>> interv_idx 

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

1559 >>> interv_idx.is_non_overlapping_monotonic 

1560 False 

1561 """ 

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

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

1564 # we already require left <= right 

1565 

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

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

1568 if self.closed == "both": 

1569 return bool( 

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

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

1572 ) 

1573 

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

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

1576 return bool( 

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

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

1579 ) 

1580 

1581 # --------------------------------------------------------------------- 

1582 # Conversion 

1583 

1584 def __array__( 

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

1586 ) -> np.ndarray: 

1587 """ 

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

1589 objects (with dtype='object') 

1590 """ 

1591 if copy is False: 

1592 raise ValueError( 

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

1594 ) 

1595 

1596 left = self._left 

1597 right = self._right 

1598 mask = self.isna() 

1599 closed = self.closed 

1600 

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

1602 for i, left_value in enumerate(left): 

1603 if mask[i]: 

1604 result[i] = np.nan 

1605 else: 

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

1607 return result 

1608 

1609 def __arrow_array__(self, type=None): 

1610 """ 

1611 Convert myself into a pyarrow Array. 

1612 """ 

1613 import pyarrow 

1614 

1615 from pandas.core.arrays.arrow.array import to_pyarrow_type 

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

1617 

1618 try: 

1619 subtype = to_pyarrow_type(self.dtype.subtype) 

1620 except (TypeError, pyarrow.ArrowNotImplementedError) as err: 

1621 raise TypeError( 

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

1623 "is not supported" 

1624 ) from err 

1625 if subtype is None: 

1626 raise TypeError( 

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

1628 "is not supported" 

1629 ) 

1630 interval_type = ArrowIntervalType(subtype, self.closed) 

1631 

1632 def _to_arrow(values) -> pyarrow.Array: 

1633 if isinstance(self.dtype.subtype, np.dtype): 

1634 return pyarrow.array(values, type=subtype, from_pandas=True) 

1635 # pyarrow would fall back on `.values`, dropping the tz; _ndarray is UTC 

1636 return pyarrow.array(values._ndarray, from_pandas=True).cast(subtype) 

1637 

1638 storage_array = pyarrow.StructArray.from_arrays( 

1639 [_to_arrow(self._left), _to_arrow(self._right)], 

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

1641 ) 

1642 mask = self.isna() 

1643 if mask.any(): 

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

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

1646 storage_array = pyarrow.StructArray.from_buffers( 

1647 storage_array.type, 

1648 len(storage_array), 

1649 [null_bitmap], 

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

1651 ) 

1652 

1653 if type is not None: 

1654 if type.equals(interval_type.storage_type): 

1655 return storage_array 

1656 elif isinstance(type, ArrowIntervalType): 

1657 # ensure we have the same subtype and closed attributes 

1658 if not type.equals(interval_type): 

1659 raise TypeError( 

1660 "Not supported to convert IntervalArray to type with " 

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

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

1663 ) 

1664 else: 

1665 raise TypeError( 

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

1667 ) 

1668 

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

1670 

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

1672 """ 

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

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

1675 

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

1677 useful for iteration or conversion to other data structures. 

1678 

1679 Parameters 

1680 ---------- 

1681 na_tuple : bool, default True 

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

1683 just return ``NA`` as ``nan``. 

1684 

1685 Returns 

1686 ------- 

1687 ndarray or Index 

1688 An ndarray of tuples representing the intervals 

1689 if `self` is an IntervalArray. 

1690 An Index of tuples representing the intervals 

1691 if `self` is an IntervalIndex. 

1692 

1693 See Also 

1694 -------- 

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

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

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

1698 

1699 Examples 

1700 -------- 

1701 For :class:`pandas.IntervalArray`: 

1702 

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

1704 >>> idx 

1705 <IntervalArray> 

1706 [(0, 1], (1, 2]] 

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

1708 >>> idx.to_tuples() 

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

1710 dtype=object) 

1711 

1712 For :class:`pandas.IntervalIndex`: 

1713 

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

1715 >>> idx 

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

1717 >>> idx.to_tuples() 

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

1719 """ 

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

1721 if not na_tuple: 

1722 # GH 18756 

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

1724 return tuples 

1725 

1726 # --------------------------------------------------------------------- 

1727 

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

1729 if self._readonly: 

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

1731 value_left, value_right = self._validate_setitem_value(value) 

1732 

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

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

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

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

1737 else: 

1738 self._left._putmask(mask, value_left) 

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

1740 self._right._putmask(mask, value_right) 

1741 

1742 def insert(self, loc: int, item: Interval[Any]) -> Self: 

1743 """ 

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

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

1746 objects and NA can be inserted into an IntervalIndex 

1747 

1748 Parameters 

1749 ---------- 

1750 loc : int 

1751 item : Interval 

1752 

1753 Returns 

1754 ------- 

1755 IntervalArray 

1756 """ 

1757 left_insert, right_insert = self._validate_scalar(item) 

1758 

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

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

1761 

1762 return self._shallow_copy(new_left, new_right) 

1763 

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

1765 new_left: np.ndarray | DatetimeArray | TimedeltaArray 

1766 new_right: np.ndarray | DatetimeArray | TimedeltaArray 

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

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

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

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

1771 else: 

1772 new_left = self._left.delete(loc) 

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

1774 new_right = self._right.delete(loc) 

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

1776 

1777 def repeat( 

1778 self, 

1779 repeats: int | Sequence[int], 

1780 axis: AxisInt | None = None, 

1781 ) -> Self: 

1782 """ 

1783 Repeat elements of an IntervalArray. 

1784 

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

1786 is repeated consecutively a given number of times. 

1787 

1788 Parameters 

1789 ---------- 

1790 repeats : int or array of ints 

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

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

1793 IntervalArray. 

1794 axis : None 

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

1796 with numpy. 

1797 

1798 Returns 

1799 ------- 

1800 IntervalArray 

1801 Newly created IntervalArray with repeated elements. 

1802 

1803 See Also 

1804 -------- 

1805 Series.repeat : Equivalent function for Series. 

1806 Index.repeat : Equivalent function for Index. 

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

1808 ExtensionArray.take : Take arbitrary positions. 

1809 

1810 Examples 

1811 -------- 

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

1813 >>> cat 

1814 ['a', 'b', 'c'] 

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

1816 >>> cat.repeat(2) 

1817 ['a', 'a', 'b', 'b', 'c', 'c'] 

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

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

1820 ['a', 'b', 'b', 'c', 'c', 'c'] 

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

1822 """ 

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

1824 left_repeat = self.left.repeat(repeats) 

1825 right_repeat = self.right.repeat(repeats) 

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

1827 

1828 def contains(self, other): 

1829 """ 

1830 Check elementwise if the Intervals contain the value. 

1831 

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

1833 of the IntervalArray. 

1834 

1835 Parameters 

1836 ---------- 

1837 other : scalar 

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

1839 

1840 Returns 

1841 ------- 

1842 boolean array 

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

1844 

1845 See Also 

1846 -------- 

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

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

1849 IntervalArray. 

1850 

1851 Examples 

1852 -------- 

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

1854 >>> intervals 

1855 <IntervalArray> 

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

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

1858 

1859 >>> intervals.contains(0.5) 

1860 array([ True, False, False]) 

1861 """ 

1862 if isinstance(other, Interval): 

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

1864 

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

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

1867 ) 

1868 

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

1870 if isinstance(values, IntervalArray): 

1871 if self.closed != values.closed: 

1872 # not comparable -> no overlap 

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

1874 

1875 if self.dtype == values.dtype: 

1876 left = self._combined 

1877 right = values._combined 

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

1879 

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

1881 values.left.dtype 

1882 ): 

1883 # not comparable -> no overlap 

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

1885 

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

1887 

1888 @property 

1889 def _combined(self) -> IntervalSide: 

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

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

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

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

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

1895 # complex128 ndarray is much more performant. 

1896 if needs_i8_conversion(left.dtype): 

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

1898 # no attribute "_concat_same_type" 

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

1900 [left, right], axis=1 

1901 ) 

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

1903 else: 

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

1905 comb.imag = right.ravel() 

1906 return comb 

1907 

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

1909 """ 

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

1911 """ 

1912 

1913 dtype = self._left.dtype 

1914 if needs_i8_conversion(dtype): 

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

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

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

1918 self._left 

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

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

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

1922 self._right 

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

1924 else: 

1925 assert isinstance(dtype, np.dtype) 

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

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

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

1929 

1930 def unique(self) -> IntervalArray: 

1931 nc = unique(self._combined) 

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

1933 

1934 

1935def _maybe_convert_platform_interval(values) -> ArrayLike: 

1936 """ 

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

1938 

1939 For example, empty lists return with integer dtype instead of object dtype, 

1940 which is prohibited for IntervalArray. 

1941 

1942 Parameters 

1943 ---------- 

1944 values : array-like 

1945 

1946 Returns 

1947 ------- 

1948 array 

1949 """ 

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

1951 # GH 19016 

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

1953 # prohibited for IntervalArray, so coerce to integer instead 

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

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

1956 # This will raise later 

1957 return values 

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

1959 values = np.asarray(values) 

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

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

1962 return values 

1963 else: 

1964 values = extract_array(values, extract_numpy=True) 

1965 

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

1967 values = np.asarray(values) 

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

1969 values = values.astype(np.int64) 

1970 return values