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

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

769 statements  

1from __future__ import annotations 

2 

3from typing import ( 

4 TYPE_CHECKING, 

5 Any, 

6 Literal, 

7 Self, 

8 cast, 

9 overload, 

10) 

11import warnings 

12 

13import numpy as np 

14 

15from pandas._config import ( 

16 is_nan_na, 

17 using_python_scalars, 

18) 

19 

20from pandas._libs import ( 

21 algos as libalgos, 

22 lib, 

23 missing as libmissing, 

24) 

25from pandas._libs.tslibs import is_supported_dtype 

26from pandas.compat import ( 

27 IS64, 

28 is_platform_windows, 

29) 

30from pandas.errors import AbstractMethodError 

31 

32from pandas.core.dtypes.astype import astype_is_view 

33from pandas.core.dtypes.base import ExtensionDtype 

34from pandas.core.dtypes.cast import ( 

35 maybe_downcast_to_dtype, 

36) 

37from pandas.core.dtypes.common import ( 

38 is_bool, 

39 is_integer_dtype, 

40 is_list_like, 

41 is_scalar, 

42 is_string_dtype, 

43 pandas_dtype, 

44) 

45from pandas.core.dtypes.dtypes import ( 

46 ArrowDtype, 

47 BaseMaskedDtype, 

48) 

49from pandas.core.dtypes.missing import ( 

50 array_equivalent, 

51 is_valid_na_for_dtype, 

52 isna, 

53 notna, 

54) 

55 

56from pandas.core import ( 

57 algorithms as algos, 

58 arraylike, 

59 missing, 

60 nanops, 

61 ops, 

62) 

63from pandas.core.algorithms import ( 

64 factorize_array, 

65 isin, 

66 map_array, 

67 mode, 

68 take, 

69) 

70from pandas.core.array_algos import ( 

71 masked_accumulations, 

72 masked_reductions, 

73) 

74from pandas.core.array_algos.quantile import quantile_with_mask 

75from pandas.core.array_algos.transforms import shift 

76from pandas.core.arraylike import OpsMixin 

77from pandas.core.arrays._utils import to_numpy_dtype_inference 

78from pandas.core.arrays.base import ExtensionArray 

79from pandas.core.construction import ( 

80 array as pd_array, 

81 ensure_wrapped_if_datetimelike, 

82 extract_array, 

83) 

84from pandas.core.indexers import ( 

85 check_array_indexer, 

86 getitem_returns_view, 

87) 

88from pandas.core.ops import invalid_comparison 

89from pandas.core.util.hashing import hash_array 

90 

91if TYPE_CHECKING: 

92 from collections.abc import Callable 

93 from collections.abc import ( 

94 Iterator, 

95 Sequence, 

96 ) 

97 from pandas import Series 

98 from pandas.core.arrays import BooleanArray 

99 from pandas._typing import ( 

100 NumpySorter, 

101 NumpyValueArrayLike, 

102 ArrayLike, 

103 AstypeArg, 

104 AxisInt, 

105 DtypeObj, 

106 FillnaOptions, 

107 InterpolateOptions, 

108 NpDtype, 

109 PositionalIndexer, 

110 Scalar, 

111 ScalarIndexer, 

112 SequenceIndexer, 

113 Shape, 

114 npt, 

115 ) 

116 from pandas._libs.missing import NAType 

117 from pandas.core.arrays import FloatingArray 

118 

119from pandas.compat.numpy import function as nv 

120 

121 

122class BaseMaskedArray(OpsMixin, ExtensionArray): 

123 """ 

124 Base class for masked arrays (which use _data and _mask to store the data). 

125 

126 numpy based 

127 """ 

128 

129 # our underlying data and mask are each ndarrays 

130 _data: np.ndarray 

131 _mask: npt.NDArray[np.bool_] 

132 

133 @classmethod 

134 def _simple_new(cls, values: np.ndarray, mask: npt.NDArray[np.bool_]) -> Self: 

135 result = BaseMaskedArray.__new__(cls) 

136 result._data = values 

137 result._mask = mask 

138 return result 

139 

140 def __init__( 

141 self, values: np.ndarray, mask: npt.NDArray[np.bool_], copy: bool = False 

142 ) -> None: 

143 # values is supposed to already be validated in the subclass 

144 if not (isinstance(mask, np.ndarray) and mask.dtype == np.bool_): 

145 raise TypeError( 

146 "mask should be boolean numpy array. Use " 

147 "the 'pd.array' function instead" 

148 ) 

149 if values.shape != mask.shape: 

150 raise ValueError("values.shape must match mask.shape") 

151 

152 if copy: 

153 values = values.copy() 

154 mask = mask.copy() 

155 

156 self._data = values 

157 self._mask = mask 

158 

159 @classmethod 

160 def _from_sequence(cls, scalars, *, dtype=None, copy: bool = False) -> Self: 

161 values, mask = cls._coerce_to_array(scalars, dtype=dtype, copy=copy) 

162 return cls(values, mask) 

163 

164 def _cast_pointwise_result(self, values) -> ArrayLike: 

165 if isna(values).all(): 

166 return type(self)._from_sequence(values, dtype=self.dtype) 

167 values = np.asarray(values, dtype=object) 

168 result = lib.maybe_convert_objects(values, convert_to_nullable_dtype=True) 

169 lkind = self.dtype.kind 

170 rkind = result.dtype.kind 

171 if (lkind in "iu" and rkind in "iu") or (lkind == rkind == "f"): 

172 result = cast(BaseMaskedArray, result) 

173 new_data = maybe_downcast_to_dtype( 

174 result._data, dtype=self.dtype.numpy_dtype 

175 ) 

176 result = type(result)(new_data, result._mask) 

177 return result 

178 

179 @classmethod 

180 def _empty(cls, shape: Shape, dtype: ExtensionDtype) -> Self: 

181 """ 

182 Create an ExtensionArray with the given shape and dtype. 

183 

184 See also 

185 -------- 

186 ExtensionDtype.empty 

187 ExtensionDtype.empty is the 'official' public version of this API. 

188 """ 

189 dtype = cast(BaseMaskedDtype, dtype) 

190 values: np.ndarray = np.empty(shape, dtype=dtype.type) 

191 values.fill(dtype._internal_fill_value) 

192 mask = np.ones(shape, dtype=bool) 

193 result = cls(values, mask) 

194 if not isinstance(result, cls) or dtype != result.dtype: 

195 raise NotImplementedError( 

196 f"Default 'empty' implementation is invalid for dtype='{dtype}'" 

197 ) 

198 return result 

199 

200 def _formatter(self, boxed: bool = False) -> Callable[[Any], str | None]: 

201 # NEP 51: https://github.com/numpy/numpy/pull/22449 

202 return str 

203 

204 @property 

205 def dtype(self) -> BaseMaskedDtype: 

206 raise AbstractMethodError(self) 

207 

208 @overload 

209 def __getitem__(self, item: ScalarIndexer) -> Any: ... 

210 

211 @overload 

212 def __getitem__(self, item: SequenceIndexer) -> Self: ... 

213 

214 def __getitem__(self, item: PositionalIndexer) -> Self | Any: 

215 item = check_array_indexer(self, item) 

216 

217 newmask = self._mask[item] 

218 if is_bool(newmask): 

219 # This is a scalar indexing 

220 if newmask: 

221 return self.dtype.na_value 

222 return self._data[item] 

223 

224 result = self._simple_new(self._data[item], newmask) 

225 if getitem_returns_view(self, item): 

226 result._readonly = self._readonly 

227 return result 

228 

229 def _pad_or_backfill( 

230 self, 

231 *, 

232 method: FillnaOptions, 

233 limit: int | None = None, 

234 limit_area: Literal["inside", "outside"] | None = None, 

235 copy: bool = True, 

236 ) -> Self: 

237 mask = self._mask 

238 

239 if mask.any(): 

240 func = missing.get_fill_func(method, ndim=self.ndim) 

241 

242 npvalues = self._data.T 

243 new_mask = mask.T 

244 if copy: 

245 npvalues = npvalues.copy() 

246 new_mask = new_mask.copy() 

247 elif limit_area is not None: 

248 mask = mask.copy() 

249 func(npvalues, limit=limit, mask=new_mask) 

250 

251 if limit_area is not None and not mask.all(): 

252 mask = mask.T 

253 neg_mask = ~mask 

254 first = neg_mask.argmax() 

255 last = len(neg_mask) - neg_mask[::-1].argmax() - 1 

256 if limit_area == "inside": 

257 new_mask[:first] |= mask[:first] 

258 new_mask[last + 1 :] |= mask[last + 1 :] 

259 elif limit_area == "outside": 

260 new_mask[first + 1 : last] |= mask[first + 1 : last] 

261 

262 if copy: 

263 return self._simple_new(npvalues.T, new_mask.T) 

264 else: 

265 return self 

266 elif copy: 

267 new_values = self.copy() 

268 else: 

269 new_values = self 

270 return new_values 

271 

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

273 """ 

274 Fill NA/NaN values using the specified method. 

275 

276 Parameters 

277 ---------- 

278 value : scalar, array-like 

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

280 Alternatively, an array-like "value" can be given. It's expected 

281 that the array-like have the same length as 'self'. 

282 limit : int, default None 

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

284 copy : bool, default True 

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

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

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

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

289 

290 Returns 

291 ------- 

292 ExtensionArray 

293 With NA/NaN filled. 

294 

295 See Also 

296 -------- 

297 api.extensions.ExtensionArray.dropna : Return ExtensionArray without 

298 NA values. 

299 api.extensions.ExtensionArray.isna : A 1-D array indicating if 

300 each value is missing. 

301 

302 Examples 

303 -------- 

304 >>> arr = pd.array([np.nan, np.nan, 2, 3, np.nan, np.nan]) 

305 >>> arr.fillna(0) 

306 <IntegerArray> 

307 [0, 0, 2, 3, 0, 0] 

308 Length: 6, dtype: Int64 

309 """ 

310 mask = self._mask 

311 if limit is not None and limit < len(self): 

312 modify = mask.cumsum() > limit 

313 if modify.any(): 

314 # Only copy mask if necessary 

315 mask = mask.copy() 

316 mask[modify] = False 

317 

318 value = missing.check_value_size(value, mask, len(self)) 

319 

320 if mask.any(): 

321 # fill with value 

322 if copy: 

323 new_values = self.copy() 

324 else: 

325 new_values = self[:] 

326 new_values[mask] = value 

327 elif copy: 

328 new_values = self.copy() 

329 else: 

330 new_values = self[:] 

331 return new_values 

332 

333 @classmethod 

334 def _coerce_to_array( 

335 cls, values, *, dtype: DtypeObj, copy: bool = False 

336 ) -> tuple[np.ndarray, np.ndarray]: 

337 raise AbstractMethodError(cls) 

338 

339 def _validate_setitem_value(self, value): 

340 """ 

341 Check if we have a scalar that we can cast losslessly. 

342 

343 Raises 

344 ------ 

345 TypeError 

346 """ 

347 kind = self.dtype.kind 

348 # TODO: get this all from np_can_hold_element? 

349 if kind == "b": 

350 if lib.is_bool(value): 

351 return value 

352 

353 elif kind == "f": 

354 if lib.is_integer(value) or lib.is_float(value): 

355 return value 

356 

357 elif lib.is_integer(value) or (lib.is_float(value) and value.is_integer()): 

358 return value 

359 # TODO: unsigned checks 

360 

361 # Note: without the "str" here, the f-string rendering raises in 

362 # py38 builds. 

363 raise TypeError(f"Invalid value '{value!s}' for dtype '{self.dtype}'") 

364 

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

366 if self._readonly: 

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

368 

369 key = check_array_indexer(self, key) 

370 

371 if is_scalar(value): 

372 if is_valid_na_for_dtype(value, self.dtype) and not ( 

373 lib.is_float(value) and not is_nan_na() 

374 ): 

375 self._mask[key] = True 

376 else: 

377 value = self._validate_setitem_value(value) 

378 self._data[key] = value 

379 self._mask[key] = False 

380 return 

381 

382 value, mask = self._coerce_to_array(value, dtype=self.dtype) 

383 

384 self._data[key] = value 

385 self._mask[key] = mask 

386 

387 def __contains__(self, key) -> bool: 

388 if isna(key) and key is not self.dtype.na_value: 

389 # GH#52840 

390 if lib.is_float(key) and is_nan_na(): 

391 key = self.dtype.na_value 

392 elif self._data.dtype.kind == "f" and lib.is_float(key): 

393 return bool((np.isnan(self._data) & ~self._mask).any()) 

394 

395 return bool(super().__contains__(key)) 

396 

397 def __iter__(self) -> Iterator: 

398 if self.ndim == 1: 

399 if not self._hasna: 

400 for val in self._data: 

401 yield val 

402 else: 

403 na_value = self.dtype.na_value 

404 for isna_, val in zip(self._mask, self._data, strict=True): 

405 if isna_: 

406 yield na_value 

407 else: 

408 yield val 

409 else: 

410 for i in range(len(self)): 

411 yield self[i] 

412 

413 def __len__(self) -> int: 

414 return len(self._data) 

415 

416 @property 

417 def shape(self) -> Shape: 

418 return self._data.shape 

419 

420 @property 

421 def ndim(self) -> int: 

422 return self._data.ndim 

423 

424 def swapaxes(self, axis1, axis2) -> Self: 

425 data = self._data.swapaxes(axis1, axis2) 

426 mask = self._mask.swapaxes(axis1, axis2) 

427 return self._simple_new(data, mask) 

428 

429 def delete(self, loc, axis: AxisInt = 0) -> Self: 

430 data = np.delete(self._data, loc, axis=axis) 

431 mask = np.delete(self._mask, loc, axis=axis) 

432 return self._simple_new(data, mask) 

433 

434 def reshape(self, *args, **kwargs) -> Self: 

435 data = self._data.reshape(*args, **kwargs) 

436 mask = self._mask.reshape(*args, **kwargs) 

437 return self._simple_new(data, mask) 

438 

439 def ravel(self, *args, **kwargs) -> Self: 

440 # TODO: need to make sure we have the same order for data/mask 

441 data = self._data.ravel(*args, **kwargs) 

442 mask = self._mask.ravel(*args, **kwargs) 

443 return type(self)(data, mask) 

444 

445 def shift(self, periods: int = 1, fill_value=None) -> Self: 

446 # NB: shift is always along axis=0 

447 axis = 0 

448 if fill_value is None: 

449 new_data = shift(self._data, periods, axis, 0) 

450 new_mask = shift(self._mask, periods, axis, True) 

451 else: 

452 new_data = shift(self._data, periods, axis, fill_value) 

453 new_mask = shift(self._mask, periods, axis, False) 

454 return type(self)(new_data, new_mask) 

455 

456 @property 

457 def T(self) -> Self: 

458 return self._simple_new(self._data.T, self._mask.T) 

459 

460 def round(self, decimals: int = 0, *args, **kwargs): 

461 """ 

462 Round each value in the array a to the given number of decimals. 

463 

464 Parameters 

465 ---------- 

466 decimals : int, default 0 

467 Number of decimal places to round to. If decimals is negative, 

468 it specifies the number of positions to the left of the decimal point. 

469 *args, **kwargs 

470 Additional arguments and keywords have no effect but might be 

471 accepted for compatibility with NumPy. 

472 

473 Returns 

474 ------- 

475 NumericArray 

476 Rounded values of the NumericArray. 

477 

478 See Also 

479 -------- 

480 numpy.around : Round values of an np.array. 

481 DataFrame.round : Round values of a DataFrame. 

482 Series.round : Round values of a Series. 

483 """ 

484 if self.dtype.kind == "b": 

485 return self 

486 nv.validate_round(args, kwargs) 

487 values = np.round(self._data, decimals=decimals, **kwargs) 

488 

489 # Usually we'll get same type as self, but ndarray[bool] casts to float 

490 return self._maybe_mask_result(values, self._mask.copy()) 

491 

492 # ------------------------------------------------------------------ 

493 # Unary Methods 

494 

495 def __invert__(self) -> Self: 

496 return self._simple_new(~self._data, self._mask.copy()) 

497 

498 def __neg__(self) -> Self: 

499 return self._simple_new(-self._data, self._mask.copy()) 

500 

501 def __pos__(self) -> Self: 

502 return self.copy() 

503 

504 def __abs__(self) -> Self: 

505 return self._simple_new(abs(self._data), self._mask.copy()) 

506 

507 # ------------------------------------------------------------------ 

508 

509 def _values_for_json(self) -> np.ndarray: 

510 return np.asarray(self, dtype=object) 

511 

512 def to_numpy( 

513 self, 

514 dtype: npt.DTypeLike | None = None, 

515 copy: bool = False, 

516 na_value: object = lib.no_default, 

517 ) -> np.ndarray: 

518 """ 

519 Convert to a NumPy Array. 

520 

521 By default converts to an object-dtype NumPy array. Specify the `dtype` and 

522 `na_value` keywords to customize the conversion. 

523 

524 Parameters 

525 ---------- 

526 dtype : dtype, default object 

527 The numpy dtype to convert to. 

528 copy : bool, default False 

529 Whether to ensure that the returned value is a not a view on 

530 the array. Note that ``copy=False`` does not *ensure* that 

531 ``to_numpy()`` is no-copy. Rather, ``copy=True`` ensure that 

532 a copy is made, even if not strictly necessary. This is typically 

533 only possible when no missing values are present and `dtype` 

534 is the equivalent numpy dtype. 

535 na_value : scalar, optional 

536 Scalar missing value indicator to use in numpy array. Defaults 

537 to the native missing value indicator of this array (pd.NA). 

538 

539 Returns 

540 ------- 

541 numpy.ndarray 

542 

543 Examples 

544 -------- 

545 An object-dtype is the default result 

546 

547 >>> a = pd.array([True, False, pd.NA], dtype="boolean") 

548 >>> a.to_numpy() 

549 array([True, False, <NA>], dtype=object) 

550 

551 When no missing values are present, an equivalent dtype can be used. 

552 

553 >>> pd.array([True, False], dtype="boolean").to_numpy(dtype="bool") 

554 array([ True, False]) 

555 >>> pd.array([1, 2], dtype="Int64").to_numpy("int64") 

556 array([1, 2]) 

557 

558 However, requesting such dtype will raise a ValueError if 

559 missing values are present and the default missing value :attr:`NA` 

560 is used. 

561 

562 >>> a = pd.array([True, False, pd.NA], dtype="boolean") 

563 >>> a 

564 <BooleanArray> 

565 [True, False, <NA>] 

566 Length: 3, dtype: boolean 

567 

568 >>> a.to_numpy(dtype="bool") 

569 Traceback (most recent call last): 

570 ... 

571 ValueError: cannot convert to bool numpy array in presence of missing values 

572 

573 Specify a valid `na_value` instead 

574 

575 >>> a.to_numpy(dtype="bool", na_value=False) 

576 array([ True, False, False]) 

577 """ 

578 hasna = self._hasna 

579 dtype, na_value = to_numpy_dtype_inference(self, dtype, na_value, hasna) 

580 if dtype is None: 

581 dtype = np.dtype(object) 

582 

583 if hasna: 

584 if ( 

585 dtype != np.dtype(object) 

586 and not is_string_dtype(dtype) 

587 and na_value is libmissing.NA 

588 ): 

589 raise ValueError( 

590 f"cannot convert to '{dtype}'-dtype NumPy array " 

591 "with missing values. Specify an appropriate 'na_value' " 

592 "for this dtype." 

593 ) 

594 # don't pass copy to astype -> always need a copy since we are mutating 

595 with warnings.catch_warnings(): 

596 warnings.filterwarnings("ignore", category=RuntimeWarning) 

597 data = self._data.astype(dtype) 

598 data[self._mask] = na_value 

599 else: 

600 with warnings.catch_warnings(): 

601 warnings.filterwarnings("ignore", category=RuntimeWarning) 

602 data = self._data.astype(dtype, copy=copy) 

603 if self._readonly and not copy and astype_is_view(self.dtype, dtype): 

604 data = data.view() 

605 data.flags.writeable = False 

606 return data 

607 

608 def tolist(self) -> list: 

609 """ 

610 Return a list of the values. 

611 

612 These are each a scalar type, which is a Python scalar 

613 (for str, int, float) or a pandas scalar 

614 (for Timestamp/Timedelta/Interval/Period) 

615 

616 Returns 

617 ------- 

618 list 

619 Python list of values in array. 

620 

621 See Also 

622 -------- 

623 Index.to_list: Return a list of the values in the Index. 

624 Series.to_list: Return a list of the values in the Series. 

625 

626 Examples 

627 -------- 

628 >>> arr = pd.array([1, 2, 3]) 

629 >>> arr.tolist() 

630 [1, 2, 3] 

631 """ 

632 if self.ndim > 1: 

633 return [x.tolist() for x in self] 

634 dtype = None if self._hasna else self._data.dtype 

635 return self.to_numpy(dtype=dtype, na_value=libmissing.NA).tolist() 

636 

637 @overload 

638 def astype(self, dtype: npt.DTypeLike, copy: bool = ...) -> np.ndarray: ... 

639 

640 @overload 

641 def astype(self, dtype: ExtensionDtype, copy: bool = ...) -> ExtensionArray: ... 

642 

643 @overload 

644 def astype(self, dtype: AstypeArg, copy: bool = ...) -> ArrayLike: ... 

645 

646 def astype(self, dtype: AstypeArg, copy: bool = True) -> ArrayLike: 

647 dtype = pandas_dtype(dtype) 

648 

649 if dtype == self.dtype: 

650 if copy: 

651 return self.copy() 

652 return self 

653 

654 # if we are astyping to another nullable masked dtype, we can fastpath 

655 if isinstance(dtype, BaseMaskedDtype): 

656 # TODO deal with NaNs for FloatingArray case 

657 with warnings.catch_warnings(): 

658 warnings.filterwarnings("ignore", category=RuntimeWarning) 

659 # TODO: Is rounding what we want long term? 

660 data = self._data.astype(dtype.numpy_dtype, copy=copy) 

661 # mask is copied depending on whether the data was copied, and 

662 # not directly depending on the `copy` keyword 

663 mask = self._mask if data is self._data else self._mask.copy() 

664 cls = dtype.construct_array_type() 

665 return cls(data, mask, copy=False) 

666 

667 if isinstance(dtype, ExtensionDtype): 

668 eacls = dtype.construct_array_type() 

669 return eacls._from_sequence(self, dtype=dtype, copy=copy) 

670 

671 na_value: float | np.datetime64 | lib.NoDefault 

672 

673 # coerce 

674 if dtype.kind == "f": 

675 # In astype, we consider dtype=float to also mean na_value=np.nan 

676 na_value = np.nan 

677 elif dtype.kind == "M": 

678 unit = np.datetime_data(dtype)[0] 

679 na_value = np.datetime64("NaT", unit) # type: ignore[call-overload] 

680 else: 

681 na_value = lib.no_default 

682 

683 # to_numpy will also raise, but we get somewhat nicer exception messages here 

684 if dtype.kind in "iu" and self._hasna: 

685 raise ValueError("cannot convert NA to integer") 

686 if dtype.kind == "b" and self._hasna: 

687 # careful: astype_nansafe converts np.nan to True 

688 raise ValueError("cannot convert float NaN to bool") 

689 

690 data = self.to_numpy(dtype=dtype, na_value=na_value, copy=copy) 

691 return data 

692 

693 __array_priority__ = 1000 # higher than ndarray so ops dispatch to us 

694 

695 def __array__( 

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

697 ) -> np.ndarray: 

698 """ 

699 the array interface, return my values 

700 We return an object array here to preserve our scalar values 

701 """ 

702 if copy is False: 

703 if not self._hasna: 

704 # special case, here we can simply return the underlying data 

705 result = np.array(self._data, dtype=dtype, copy=copy) 

706 # If the ExtensionArray is readonly, make the numpy array readonly too 

707 if self._readonly: 

708 result = result.view() 

709 result.flags.writeable = False 

710 return result 

711 raise ValueError( 

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

713 ) 

714 

715 if copy is None: 

716 copy = False # The NumPy copy=False meaning is different here. 

717 return self.to_numpy(dtype=dtype, copy=copy) 

718 

719 _HANDLED_TYPES: tuple[type, ...] 

720 

721 def __array_ufunc__(self, ufunc: np.ufunc, method: str, *inputs, **kwargs): 

722 # For MaskedArray inputs, we apply the ufunc to ._data 

723 # and mask the result. 

724 

725 out = kwargs.get("out", ()) 

726 

727 for x in inputs + out: 

728 if not isinstance(x, (*self._HANDLED_TYPES, BaseMaskedArray)): 

729 return NotImplemented 

730 

731 # for binary ops, use our custom dunder methods 

732 result = arraylike.maybe_dispatch_ufunc_to_dunder_op( 

733 self, ufunc, method, *inputs, **kwargs 

734 ) 

735 if result is not NotImplemented: 

736 return result 

737 

738 if "out" in kwargs: 

739 # e.g. test_ufunc_with_out 

740 return arraylike.dispatch_ufunc_with_out( 

741 self, ufunc, method, *inputs, **kwargs 

742 ) 

743 

744 if method == "reduce": 

745 result = arraylike.dispatch_reduction_ufunc( 

746 self, ufunc, method, *inputs, **kwargs 

747 ) 

748 if result is not NotImplemented: 

749 return result 

750 

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

752 inputs2 = [] 

753 for x in inputs: 

754 if isinstance(x, BaseMaskedArray): 

755 mask |= x._mask 

756 inputs2.append(x._data) 

757 else: 

758 inputs2.append(x) 

759 

760 def reconstruct(x: np.ndarray): 

761 # we don't worry about scalar `x` here, since we 

762 # raise for reduce up above. 

763 from pandas.core.arrays import ( 

764 BooleanArray, 

765 FloatingArray, 

766 IntegerArray, 

767 ) 

768 

769 if x.dtype.kind == "b": 

770 m = mask.copy() 

771 return BooleanArray(x, m) 

772 elif x.dtype.kind in "iu": 

773 m = mask.copy() 

774 return IntegerArray(x, m) 

775 elif x.dtype.kind == "f": 

776 m = mask.copy() 

777 if x.dtype == np.float16: 

778 # reached in e.g. np.sqrt on BooleanArray 

779 # we don't support float16 

780 x = x.astype(np.float32) 

781 if is_nan_na(): 

782 m[np.isnan(x)] = True 

783 return FloatingArray(x, m) 

784 else: 

785 x[mask] = np.nan 

786 return x 

787 

788 result = getattr(ufunc, method)(*inputs2, **kwargs) 

789 if ufunc.nout > 1: 

790 # e.g. np.divmod 

791 return tuple(reconstruct(x) for x in result) 

792 elif method == "reduce": 

793 # e.g. np.add.reduce; test_ufunc_reduce_raises 

794 if self._mask.any(): 

795 return self._na_value 

796 return result 

797 else: 

798 return reconstruct(result) 

799 

800 def __arrow_array__(self, type=None): 

801 """ 

802 Convert myself into a pyarrow Array. 

803 """ 

804 import pyarrow as pa 

805 

806 return pa.array(self._data, mask=self._mask, type=type) 

807 

808 @property 

809 def _hasna(self) -> bool: 

810 # Note: this is expensive right now! The hope is that we can 

811 # make this faster by having an optional mask, but not have to change 

812 # source code using it.. 

813 

814 return bool(self._mask.any()) 

815 

816 def _propagate_mask( 

817 self, mask: npt.NDArray[np.bool_] | None, other 

818 ) -> npt.NDArray[np.bool_]: 

819 if mask is None: 

820 mask = self._mask.copy() # TODO: need test for BooleanArray needing a copy 

821 if other is libmissing.NA: 

822 # GH#45421 don't alter inplace 

823 mask = mask | True 

824 elif is_list_like(other) and len(other) == len(mask): 

825 mask = mask | isna(other) 

826 else: 

827 mask = self._mask | mask 

828 return mask 

829 

830 def _arith_method(self, other, op): 

831 op_name = op.__name__ 

832 omask = None 

833 

834 if ( 

835 not hasattr(other, "dtype") 

836 and is_list_like(other) 

837 and len(other) == len(self) 

838 ): 

839 # Try inferring masked dtype instead of casting to object 

840 other = pd_array(other) 

841 other = extract_array(other, extract_numpy=True) 

842 

843 if isinstance(other, BaseMaskedArray): 

844 other, omask = other._data, other._mask 

845 

846 elif is_list_like(other): 

847 if not isinstance(other, ExtensionArray): 

848 other = np.asarray(other) 

849 if other.ndim > 1: 

850 raise NotImplementedError("can only perform ops with 1-d structures") 

851 

852 # We wrap the non-masked arithmetic logic used for numpy dtypes 

853 # in Series/Index arithmetic ops. 

854 other = ops.maybe_prepare_scalar_for_op(other, (len(self),)) 

855 pd_op = ops.get_array_op(op) 

856 other = ensure_wrapped_if_datetimelike(other) 

857 

858 if isinstance(other, ExtensionArray) and isinstance(other.dtype, ArrowDtype): 

859 # GH#58602 

860 return NotImplemented 

861 

862 if op_name in {"pow", "rpow"} and isinstance(other, np.bool_): 

863 # Avoid DeprecationWarning: In future, it will be an error 

864 # for 'np.bool_' scalars to be interpreted as an index 

865 # e.g. test_array_scalar_like_equivalence 

866 other = bool(other) 

867 

868 mask = self._propagate_mask(omask, other) 

869 

870 if other is libmissing.NA: 

871 result = np.ones_like(self._data) 

872 if self.dtype.kind == "b": 

873 if op_name in { 

874 "floordiv", 

875 "rfloordiv", 

876 "pow", 

877 "rpow", 

878 "truediv", 

879 "rtruediv", 

880 }: 

881 # GH#41165 Try to match non-masked Series behavior 

882 # This is still imperfect GH#46043 

883 raise NotImplementedError( 

884 f"operator '{op_name}' not implemented for bool dtypes" 

885 ) 

886 if op_name in {"mod", "rmod"}: 

887 dtype = "int8" 

888 else: 

889 dtype = "bool" 

890 result = result.astype(dtype) 

891 elif "truediv" in op_name and self.dtype.kind != "f": 

892 # The actual data here doesn't matter since the mask 

893 # will be all-True, but since this is division, we want 

894 # to end up with floating dtype. 

895 result = result.astype(np.float64) 

896 elif op_name in {"divmod", "rdivmod"}: 

897 # GH#62196 

898 res = self._maybe_mask_result(result, mask) 

899 return res, res.copy() 

900 else: 

901 # Make sure we do this before the "pow" mask checks 

902 # to get an expected exception message on shape mismatch. 

903 if self.dtype.kind in "iu" and op_name in ["floordiv", "mod"]: 

904 # TODO(GH#30188) ATM we don't match the behavior of non-masked 

905 # types with respect to floordiv-by-zero 

906 pd_op = op 

907 

908 with np.errstate(all="ignore"): 

909 result = pd_op(self._data, other) 

910 

911 if op_name == "pow": 

912 # 1 ** x is 1. 

913 mask = np.where((self._data == 1) & ~self._mask, False, mask) 

914 # x ** 0 is 1. 

915 if omask is not None: 

916 mask = np.where((other == 0) & ~omask, False, mask) 

917 elif other is not libmissing.NA: 

918 mask = np.where(other == 0, False, mask) 

919 

920 elif op_name == "rpow": 

921 # 1 ** x is 1. 

922 if omask is not None: 

923 mask = np.where((other == 1) & ~omask, False, mask) 

924 elif other is not libmissing.NA: 

925 mask = np.where(other == 1, False, mask) 

926 # x ** 0 is 1. 

927 mask = np.where((self._data == 0) & ~self._mask, False, mask) 

928 

929 return self._maybe_mask_result(result, mask) 

930 

931 _logical_method = _arith_method 

932 

933 def _cmp_method(self, other, op) -> BooleanArray: 

934 from pandas.core.arrays import BooleanArray 

935 

936 mask = None 

937 

938 if isinstance(other, ExtensionArray) and isinstance(other.dtype, ArrowDtype): 

939 # GH#58602 

940 return NotImplemented 

941 

942 elif isinstance(other, BaseMaskedArray): 

943 other, mask = other._data, other._mask 

944 

945 elif is_list_like(other): 

946 other = np.asarray(other) 

947 if other.ndim > 1: 

948 raise NotImplementedError("can only perform ops with 1-d structures") 

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

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

951 

952 if other is libmissing.NA: 

953 # numpy does not handle pd.NA well as "other" scalar (it returns 

954 # a scalar False instead of an array) 

955 # This may be fixed by NA.__array_ufunc__. Revisit this check 

956 # once that's implemented. 

957 result = np.zeros(self._data.shape, dtype="bool") 

958 mask = np.ones(self._data.shape, dtype="bool") 

959 else: 

960 with warnings.catch_warnings(): 

961 # numpy may show a FutureWarning or DeprecationWarning: 

962 # elementwise comparison failed; returning scalar instead, 

963 # but in the future will perform elementwise comparison 

964 # before returning NotImplemented. We fall back to the correct 

965 # behavior today, so that should be fine to ignore. 

966 warnings.filterwarnings("ignore", "elementwise", FutureWarning) 

967 warnings.filterwarnings("ignore", "elementwise", DeprecationWarning) 

968 method = getattr(self._data, f"__{op.__name__}__") 

969 result = method(other) 

970 

971 if result is NotImplemented: 

972 result = invalid_comparison(self._data, other, op) 

973 

974 mask = self._propagate_mask(mask, other) 

975 return BooleanArray(result, mask, copy=False) 

976 

977 def _maybe_mask_result( 

978 self, result: np.ndarray | tuple[np.ndarray, np.ndarray], mask: np.ndarray 

979 ): 

980 """ 

981 Parameters 

982 ---------- 

983 result : array-like or tuple[array-like] 

984 mask : array-like bool 

985 """ 

986 if isinstance(result, tuple): 

987 # i.e. divmod 

988 div, mod = result 

989 return ( 

990 self._maybe_mask_result(div, mask), 

991 self._maybe_mask_result(mod, mask), 

992 ) 

993 

994 if result.dtype.kind == "f": 

995 from pandas.core.arrays import FloatingArray 

996 

997 if is_nan_na(): 

998 mask[np.isnan(result)] = True 

999 

1000 return FloatingArray(result, mask, copy=False) 

1001 

1002 elif result.dtype.kind == "b": 

1003 from pandas.core.arrays import BooleanArray 

1004 

1005 return BooleanArray(result, mask, copy=False) 

1006 

1007 elif lib.is_np_dtype(result.dtype, "m") and is_supported_dtype(result.dtype): 

1008 # e.g. test_numeric_arr_mul_tdscalar_numexpr_path 

1009 from pandas.core.arrays import TimedeltaArray 

1010 

1011 unit = np.datetime_data(result.dtype)[0] 

1012 result[mask] = np.timedelta64("NaT", unit) # type: ignore[call-overload] 

1013 

1014 if not isinstance(result, TimedeltaArray): 

1015 return TimedeltaArray._simple_new(result, dtype=result.dtype) 

1016 

1017 return result 

1018 

1019 elif result.dtype.kind in "iu": 

1020 from pandas.core.arrays import IntegerArray 

1021 

1022 return IntegerArray(result, mask, copy=False) 

1023 

1024 elif result.dtype == object: 

1025 result[mask] = self.dtype.na_value 

1026 return result 

1027 else: 

1028 result[mask] = np.nan 

1029 return result 

1030 

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

1032 return self._mask.copy() 

1033 

1034 @property 

1035 def _na_value(self): 

1036 return self.dtype.na_value 

1037 

1038 @property 

1039 def nbytes(self) -> int: 

1040 return self._data.nbytes + self._mask.nbytes 

1041 

1042 @classmethod 

1043 def _concat_same_type( 

1044 cls, 

1045 to_concat: Sequence[Self], 

1046 axis: AxisInt = 0, 

1047 ) -> Self: 

1048 data = np.concatenate([x._data for x in to_concat], axis=axis) 

1049 mask = np.concatenate([x._mask for x in to_concat], axis=axis) 

1050 return cls(data, mask) 

1051 

1052 def _hash_pandas_object( 

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

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

1055 hashed_array = hash_array( 

1056 self._data, encoding=encoding, hash_key=hash_key, categorize=categorize 

1057 ) 

1058 hashed_array[self.isna()] = hash(self.dtype.na_value) 

1059 return hashed_array 

1060 

1061 def take( 

1062 self, 

1063 indexer, 

1064 *, 

1065 allow_fill: bool = False, 

1066 fill_value: Scalar | None = None, 

1067 axis: AxisInt = 0, 

1068 ) -> Self: 

1069 # we always fill with 1 internally 

1070 # to avoid upcasting 

1071 data_fill_value = ( 

1072 self.dtype._internal_fill_value if isna(fill_value) else fill_value 

1073 ) 

1074 result = take( 

1075 self._data, 

1076 indexer, 

1077 fill_value=data_fill_value, 

1078 allow_fill=allow_fill, 

1079 axis=axis, 

1080 ) 

1081 

1082 mask = take( 

1083 self._mask, indexer, fill_value=True, allow_fill=allow_fill, axis=axis 

1084 ) 

1085 

1086 # if we are filling 

1087 # we only fill where the indexer is null 

1088 # not existing missing values 

1089 # TODO(jreback) what if we have a non-na float as a fill value? 

1090 if allow_fill and notna(fill_value): 

1091 fill_mask = np.asarray(indexer) == -1 

1092 result[fill_mask] = fill_value 

1093 mask = mask ^ fill_mask 

1094 

1095 return self._simple_new(result, mask) 

1096 

1097 # error: Return type "BooleanArray" of "isin" incompatible with return type 

1098 # "ndarray" in supertype "ExtensionArray" 

1099 def isin(self, values: ArrayLike) -> BooleanArray: # type: ignore[override] 

1100 from pandas.core.arrays import BooleanArray 

1101 

1102 # algorithms.isin will eventually convert values to an ndarray, so no extra 

1103 # cost to doing it here first 

1104 values_arr = np.asarray(values) 

1105 result = isin(self._data, values_arr) 

1106 

1107 if self._hasna: 

1108 values_have_NA = values_arr.dtype == object and any( 

1109 val is self.dtype.na_value for val in values_arr 

1110 ) 

1111 

1112 # For now, NA does not propagate so set result according to presence of NA, 

1113 # see https://github.com/pandas-dev/pandas/pull/38379 for some discussion 

1114 result[self._mask] = values_have_NA 

1115 

1116 mask = np.zeros(self._data.shape, dtype=bool) 

1117 return BooleanArray(result, mask, copy=False) 

1118 

1119 def copy(self) -> Self: 

1120 data = self._data.copy() 

1121 mask = self._mask.copy() 

1122 return self._simple_new(data, mask) 

1123 

1124 def _rank( 

1125 self, 

1126 *, 

1127 axis: AxisInt = 0, 

1128 method: str = "average", 

1129 na_option: str = "keep", 

1130 ascending: bool = True, 

1131 pct: bool = False, 

1132 ): 

1133 # GH#62043 Avoid going through copy-making ensure_data in algorithms.rank 

1134 if axis != 0 or self.ndim != 1: 

1135 raise NotImplementedError 

1136 

1137 from pandas.core.arrays import FloatingArray 

1138 

1139 data = self._data 

1140 if data.dtype.kind == "b": 

1141 data = data.view("uint8") 

1142 

1143 result = libalgos.rank_1d( 

1144 data, 

1145 is_datetimelike=False, 

1146 ties_method=method, 

1147 ascending=ascending, 

1148 na_option=na_option, 

1149 pct=pct, 

1150 mask=self.isna(), 

1151 ) 

1152 if na_option in ["top", "bottom"]: 

1153 mask = np.zeros(self.shape, dtype=bool) 

1154 else: 

1155 mask = self._mask.copy() 

1156 

1157 if method != "average" and not pct: 

1158 if na_option not in ["top", "bottom"]: 

1159 result[self._mask] = 0 # avoid warning on casting 

1160 result = result.astype("uint64", copy=False) 

1161 from pandas.core.arrays import IntegerArray 

1162 

1163 return IntegerArray(result, mask=mask) 

1164 

1165 return FloatingArray(result, mask=mask) 

1166 

1167 def duplicated( 

1168 self, keep: Literal["first", "last", False] = "first" 

1169 ) -> npt.NDArray[np.bool_]: 

1170 """ 

1171 Return boolean ndarray denoting duplicate values. 

1172 

1173 Parameters 

1174 ---------- 

1175 keep : {'first', 'last', False}, default 'first' 

1176 - ``first`` : Mark duplicates as ``True`` except for the first occurrence. 

1177 - ``last`` : Mark duplicates as ``True`` except for the last occurrence. 

1178 - False : Mark all duplicates as ``True``. 

1179 

1180 Returns 

1181 ------- 

1182 ndarray[bool] 

1183 With true in indices where elements are duplicated and false otherwise. 

1184 

1185 See Also 

1186 -------- 

1187 DataFrame.duplicated : Return boolean Series denoting 

1188 duplicate rows. 

1189 Series.duplicated : Indicate duplicate Series values. 

1190 api.extensions.ExtensionArray.unique : Compute the ExtensionArray 

1191 of unique values. 

1192 

1193 Examples 

1194 -------- 

1195 >>> pd.array([1, 1, 2, 3, 3], dtype="Int64").duplicated() 

1196 array([False, True, False, False, True]) 

1197 """ 

1198 values = self._data 

1199 mask = self._mask 

1200 return algos.duplicated(values, keep=keep, mask=mask) 

1201 

1202 def unique(self) -> Self: 

1203 """ 

1204 Compute the BaseMaskedArray of unique values. 

1205 

1206 Returns 

1207 ------- 

1208 uniques : BaseMaskedArray 

1209 """ 

1210 uniques, mask = algos.unique_with_mask(self._data, self._mask) 

1211 return self._simple_new(uniques, mask) 

1212 

1213 def searchsorted( 

1214 self, 

1215 value: NumpyValueArrayLike | ExtensionArray, 

1216 side: Literal["left", "right"] = "left", 

1217 sorter: NumpySorter | None = None, 

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

1219 """ 

1220 Find indices where elements should be inserted to maintain order. 

1221 

1222 Find the indices into a sorted array `self` (a) such that, if the 

1223 corresponding elements in `value` were inserted before the indices, 

1224 the order of `self` would be preserved. 

1225 

1226 Assuming that `self` is sorted: 

1227 

1228 ====== ================================ 

1229 `side` returned index `i` satisfies 

1230 ====== ================================ 

1231 left ``self[i-1] < value <= self[i]`` 

1232 right ``self[i-1] <= value < self[i]`` 

1233 ====== ================================ 

1234 

1235 Parameters 

1236 ---------- 

1237 value : array-like, list or scalar 

1238 Value(s) to insert into `self`. 

1239 side : {'left', 'right'}, optional 

1240 If 'left', the index of the first suitable location found is given. 

1241 If 'right', return the last such index. If there is no suitable 

1242 index, return either 0 or N (where N is the length of `self`). 

1243 sorter : 1-D array-like, optional 

1244 Optional array of integer indices that sort array a into ascending 

1245 order. They are typically the result of argsort. 

1246 

1247 Returns 

1248 ------- 

1249 array of ints or int 

1250 If value is array-like, array of insertion points. 

1251 If value is scalar, a single integer. 

1252 

1253 See Also 

1254 -------- 

1255 numpy.searchsorted : Similar method from NumPy. 

1256 

1257 Examples 

1258 -------- 

1259 >>> arr = pd.array([1, 2, 3, 5]) 

1260 >>> arr.searchsorted([4]) 

1261 array([3]) 

1262 """ 

1263 if self._hasna: 

1264 raise ValueError( 

1265 "searchsorted requires array to be sorted, which is impossible " 

1266 "with NAs present." 

1267 ) 

1268 if isinstance(value, ExtensionArray): 

1269 value = value.astype(object) 

1270 # Base class searchsorted would cast to object, which is *much* slower. 

1271 return self._data.searchsorted(value, side=side, sorter=sorter) 

1272 

1273 def factorize( 

1274 self, 

1275 use_na_sentinel: bool = True, 

1276 ) -> tuple[np.ndarray, ExtensionArray]: 

1277 """ 

1278 Encode the extension array as an enumerated type. 

1279 

1280 Parameters 

1281 ---------- 

1282 use_na_sentinel : bool, default True 

1283 If True, the sentinel -1 will be used for NaN values. If False, 

1284 NaN values will be encoded as non-negative integers and will not drop the 

1285 NaN from the uniques of the values. 

1286 

1287 Returns 

1288 ------- 

1289 codes : ndarray 

1290 An integer NumPy array that's an indexer into the original 

1291 ExtensionArray. 

1292 uniques : ExtensionArray 

1293 An ExtensionArray containing the unique values of `self`. 

1294 

1295 .. note:: 

1296 

1297 uniques will *not* contain an entry for the NA value of 

1298 the ExtensionArray if there are any missing values present 

1299 in `self`. 

1300 

1301 See Also 

1302 -------- 

1303 factorize : Top-level factorize method that dispatches here. 

1304 

1305 Notes 

1306 ----- 

1307 :meth:`pandas.factorize` offers a `sort` keyword as well. 

1308 

1309 Examples 

1310 -------- 

1311 >>> idx1 = pd.PeriodIndex( 

1312 ... ["2014-01", "2014-01", "2014-02", "2014-02", "2014-03", "2014-03"], 

1313 ... freq="M", 

1314 ... ) 

1315 >>> arr, idx = idx1.factorize() 

1316 >>> arr 

1317 array([0, 0, 1, 1, 2, 2]) 

1318 >>> idx 

1319 PeriodIndex(['2014-01', '2014-02', '2014-03'], dtype='period[M]') 

1320 """ 

1321 arr = self._data 

1322 mask = self._mask 

1323 

1324 # Use a sentinel for na; recode and add NA to uniques if necessary below 

1325 codes, uniques = factorize_array(arr, use_na_sentinel=True, mask=mask) 

1326 

1327 # check that factorize_array correctly preserves dtype. 

1328 assert uniques.dtype == self.dtype.numpy_dtype, (uniques.dtype, self.dtype) 

1329 

1330 has_na = mask.any() 

1331 if use_na_sentinel or not has_na: 

1332 size = len(uniques) 

1333 else: 

1334 # Make room for an NA value 

1335 size = len(uniques) + 1 

1336 uniques_mask = np.zeros(size, dtype=bool) 

1337 if not use_na_sentinel and has_na: 

1338 na_index = mask.argmax() 

1339 # Insert na with the proper code 

1340 if na_index == 0: 

1341 na_code = np.intp(0) 

1342 else: 

1343 na_code = codes[:na_index].max() + 1 

1344 codes[codes >= na_code] += 1 

1345 codes[codes == -1] = na_code 

1346 # dummy value for uniques; not used since uniques_mask will be True 

1347 uniques = np.insert(uniques, na_code, 0) 

1348 uniques_mask[na_code] = True 

1349 uniques_ea = self._simple_new(uniques, uniques_mask) 

1350 

1351 return codes, uniques_ea 

1352 

1353 def _values_for_argsort(self) -> np.ndarray: 

1354 """ 

1355 Return values for sorting. 

1356 

1357 Returns 

1358 ------- 

1359 ndarray 

1360 The transformed values should maintain the ordering between values 

1361 within the array. 

1362 

1363 See Also 

1364 -------- 

1365 ExtensionArray.argsort : Return the indices that would sort this array. 

1366 

1367 Notes 

1368 ----- 

1369 The caller is responsible for *not* modifying these values in-place, so 

1370 it is safe for implementers to give views on ``self``. 

1371 

1372 Functions that use this (e.g. ``ExtensionArray.argsort``) should ignore 

1373 entries with missing values in the original array (according to 

1374 ``self.isna()``). This means that the corresponding entries in the returned 

1375 array don't need to be modified to sort correctly. 

1376 

1377 Examples 

1378 -------- 

1379 In most cases, this is the underlying Numpy array of the ``ExtensionArray``: 

1380 

1381 >>> arr = pd.array([1, 2, 3]) 

1382 >>> arr._values_for_argsort() 

1383 array([1, 2, 3]) 

1384 """ 

1385 return self._data 

1386 

1387 def value_counts(self, dropna: bool = True) -> Series: 

1388 """ 

1389 Returns a Series containing counts of each unique value. 

1390 

1391 Parameters 

1392 ---------- 

1393 dropna : bool, default True 

1394 Don't include counts of missing values. 

1395 

1396 Returns 

1397 ------- 

1398 counts : Series 

1399 

1400 See Also 

1401 -------- 

1402 Series.value_counts 

1403 """ 

1404 from pandas import ( 

1405 Index, 

1406 Series, 

1407 ) 

1408 from pandas.arrays import IntegerArray 

1409 

1410 keys, value_counts, na_counter = algos.value_counts_arraylike( 

1411 self._data, dropna=dropna, mask=self._mask 

1412 ) 

1413 mask_index = np.zeros((len(value_counts),), dtype=np.bool_) 

1414 mask = mask_index.copy() 

1415 

1416 if na_counter > 0: 

1417 mask_index[-1] = True 

1418 

1419 arr = IntegerArray(value_counts, mask) 

1420 index = Index( 

1421 self.dtype.construct_array_type()( 

1422 keys, # type: ignore[arg-type] 

1423 mask_index, 

1424 ), 

1425 copy=False, 

1426 ) 

1427 return Series(arr, index=index, name="count", copy=False) 

1428 

1429 def _mode(self, dropna: bool = True) -> Self: 

1430 result, res_mask = mode(self._data, dropna=dropna, mask=self._mask) 

1431 result = type(self)(result, res_mask) 

1432 return result[result.argsort()] 

1433 

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

1435 """ 

1436 Return if another array is equivalent to this array. 

1437 

1438 Equivalent means that both arrays have the same shape and dtype, and 

1439 all values compare equal. Missing values in the same location are 

1440 considered equal (in contrast with normal equality). 

1441 

1442 Parameters 

1443 ---------- 

1444 other : ExtensionArray 

1445 Array to compare to this Array. 

1446 

1447 Returns 

1448 ------- 

1449 boolean 

1450 Whether the arrays are equivalent. 

1451 

1452 See Also 

1453 -------- 

1454 numpy.array_equal : Equivalent method for numpy array. 

1455 Series.equals : Equivalent method for Series. 

1456 DataFrame.equals : Equivalent method for DataFrame. 

1457 

1458 Examples 

1459 -------- 

1460 >>> arr1 = pd.array([1, 2, np.nan]) 

1461 >>> arr2 = pd.array([1, 2, np.nan]) 

1462 >>> arr1.equals(arr2) 

1463 True 

1464 

1465 >>> arr1 = pd.array([1, 3, np.nan]) 

1466 >>> arr2 = pd.array([1, 2, np.nan]) 

1467 >>> arr1.equals(arr2) 

1468 False 

1469 """ 

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

1471 return False 

1472 if other.dtype != self.dtype: 

1473 return False 

1474 

1475 # GH#44382 if e.g. self[1] is np.nan and other[1] is pd.NA, we are NOT 

1476 # equal. 

1477 if not np.array_equal(self._mask, other._mask): 

1478 return False 

1479 

1480 left = self._data[~self._mask] 

1481 right = other._data[~other._mask] 

1482 return array_equivalent(left, right, strict_nan=True, dtype_equal=True) 

1483 

1484 def _quantile( 

1485 self, qs: npt.NDArray[np.float64], interpolation: str 

1486 ) -> BaseMaskedArray: 

1487 """ 

1488 Dispatch to quantile_with_mask, needed because we do not have 

1489 _from_factorized. 

1490 

1491 Notes 

1492 ----- 

1493 We assume that all impacted cases are 1D-only. 

1494 """ 

1495 res = quantile_with_mask( 

1496 self._data, 

1497 mask=self._mask, 

1498 # TODO(GH#40932): na_value_for_dtype(self.dtype.numpy_dtype) 

1499 # instead of np.nan 

1500 fill_value=np.nan, 

1501 qs=qs, 

1502 interpolation=interpolation, 

1503 ) 

1504 

1505 if self._hasna: 

1506 # Our result mask is all-False unless we are all-NA, in which 

1507 # case it is all-True. 

1508 if self.ndim == 2: 

1509 # I think this should be out_mask=self.isna().all(axis=1) 

1510 # but am holding off until we have tests 

1511 raise NotImplementedError 

1512 if self.isna().all(): 

1513 out_mask = np.ones(res.shape, dtype=bool) 

1514 

1515 if is_integer_dtype(self.dtype): 

1516 # We try to maintain int dtype if possible for not all-na case 

1517 # as well 

1518 res = np.zeros(res.shape, dtype=self.dtype.numpy_dtype) 

1519 else: 

1520 out_mask = np.zeros(res.shape, dtype=bool) 

1521 else: 

1522 out_mask = np.zeros(res.shape, dtype=bool) 

1523 return self._maybe_mask_result(res, mask=out_mask) 

1524 

1525 # ------------------------------------------------------------------ 

1526 # Reductions 

1527 

1528 def _reduce( 

1529 self, name: str, *, skipna: bool = True, keepdims: bool = False, **kwargs 

1530 ): 

1531 if name in {"any", "all", "min", "max", "sum", "prod", "mean", "var", "std"}: 

1532 result = getattr(self, name)(skipna=skipna, **kwargs) 

1533 else: 

1534 # median, skew, kurt, sem 

1535 data = self._data 

1536 mask = self._mask 

1537 op = getattr(nanops, f"nan{name}") 

1538 axis = kwargs.pop("axis", None) 

1539 result = op(data, axis=axis, skipna=skipna, mask=mask, **kwargs) 

1540 

1541 if keepdims: 

1542 if isna(result): 

1543 return self._wrap_na_result(name=name, axis=0, mask_size=(1,)) 

1544 else: 

1545 if using_python_scalars(): 

1546 result = np.array([result]) 

1547 else: 

1548 result = result.reshape(1) 

1549 mask = np.zeros(1, dtype=bool) 

1550 return self._maybe_mask_result(result, mask) 

1551 

1552 if isna(result): 

1553 return libmissing.NA 

1554 else: 

1555 return result 

1556 

1557 def _wrap_reduction_result(self, name: str, result, *, skipna, axis): 

1558 if isinstance(result, np.ndarray): 

1559 if skipna: 

1560 # we only retain mask for all-NA rows/columns 

1561 mask = self._mask.all(axis=axis) 

1562 else: 

1563 mask = self._mask.any(axis=axis) 

1564 

1565 return self._maybe_mask_result(result, mask) 

1566 return result 

1567 

1568 def _wrap_na_result(self, *, name, axis, mask_size): 

1569 mask = np.ones(mask_size, dtype=bool) 

1570 

1571 float_dtyp = "float32" if self.dtype == "Float32" else "float64" 

1572 if name in ["mean", "median", "var", "std", "skew", "kurt", "sem"]: 

1573 np_dtype = float_dtyp 

1574 elif name in ["min", "max"] or self.dtype.itemsize == 8: 

1575 np_dtype = self.dtype.numpy_dtype.name 

1576 else: 

1577 is_windows_or_32bit = is_platform_windows() or not IS64 

1578 int_dtyp = "int32" if is_windows_or_32bit else "int64" 

1579 uint_dtyp = "uint32" if is_windows_or_32bit else "uint64" 

1580 np_dtype = {"b": int_dtyp, "i": int_dtyp, "u": uint_dtyp, "f": float_dtyp}[ 

1581 self.dtype.kind 

1582 ] 

1583 

1584 value = np.array([1], dtype=np_dtype) 

1585 return self._maybe_mask_result(value, mask=mask) 

1586 

1587 def _wrap_min_count_reduction_result( 

1588 self, name: str, result, *, skipna, min_count, axis 

1589 ): 

1590 if min_count == 0 and isinstance(result, np.ndarray): 

1591 return self._maybe_mask_result(result, np.zeros(result.shape, dtype=bool)) 

1592 return self._wrap_reduction_result(name, result, skipna=skipna, axis=axis) 

1593 

1594 def sum( 

1595 self, 

1596 *, 

1597 skipna: bool = True, 

1598 min_count: int = 0, 

1599 axis: AxisInt | None = 0, 

1600 **kwargs, 

1601 ): 

1602 nv.validate_sum((), kwargs) 

1603 

1604 result = masked_reductions.sum( 

1605 self._data, 

1606 self._mask, 

1607 skipna=skipna, 

1608 min_count=min_count, 

1609 axis=axis, 

1610 ) 

1611 return self._wrap_min_count_reduction_result( 

1612 "sum", result, skipna=skipna, min_count=min_count, axis=axis 

1613 ) 

1614 

1615 def prod( 

1616 self, 

1617 *, 

1618 skipna: bool = True, 

1619 min_count: int = 0, 

1620 axis: AxisInt | None = 0, 

1621 **kwargs, 

1622 ): 

1623 nv.validate_prod((), kwargs) 

1624 

1625 result = masked_reductions.prod( 

1626 self._data, 

1627 self._mask, 

1628 skipna=skipna, 

1629 min_count=min_count, 

1630 axis=axis, 

1631 ) 

1632 return self._wrap_min_count_reduction_result( 

1633 "prod", result, skipna=skipna, min_count=min_count, axis=axis 

1634 ) 

1635 

1636 def mean(self, *, skipna: bool = True, axis: AxisInt | None = 0, **kwargs): 

1637 nv.validate_mean((), kwargs) 

1638 result = masked_reductions.mean( 

1639 self._data, 

1640 self._mask, 

1641 skipna=skipna, 

1642 axis=axis, 

1643 ) 

1644 return self._wrap_reduction_result("mean", result, skipna=skipna, axis=axis) 

1645 

1646 def var( 

1647 self, *, skipna: bool = True, axis: AxisInt | None = 0, ddof: int = 1, **kwargs 

1648 ): 

1649 nv.validate_stat_ddof_func((), kwargs, fname="var") 

1650 result = masked_reductions.var( 

1651 self._data, 

1652 self._mask, 

1653 skipna=skipna, 

1654 axis=axis, 

1655 ddof=ddof, 

1656 ) 

1657 return self._wrap_reduction_result("var", result, skipna=skipna, axis=axis) 

1658 

1659 def std( 

1660 self, *, skipna: bool = True, axis: AxisInt | None = 0, ddof: int = 1, **kwargs 

1661 ): 

1662 nv.validate_stat_ddof_func((), kwargs, fname="std") 

1663 result = masked_reductions.std( 

1664 self._data, 

1665 self._mask, 

1666 skipna=skipna, 

1667 axis=axis, 

1668 ddof=ddof, 

1669 ) 

1670 return self._wrap_reduction_result("std", result, skipna=skipna, axis=axis) 

1671 

1672 def min(self, *, skipna: bool = True, axis: AxisInt | None = 0, **kwargs): 

1673 nv.validate_min((), kwargs) 

1674 result = masked_reductions.min( 

1675 self._data, 

1676 self._mask, 

1677 skipna=skipna, 

1678 axis=axis, 

1679 ) 

1680 return self._wrap_reduction_result("min", result, skipna=skipna, axis=axis) 

1681 

1682 def max(self, *, skipna: bool = True, axis: AxisInt | None = 0, **kwargs): 

1683 nv.validate_max((), kwargs) 

1684 result = masked_reductions.max( 

1685 self._data, 

1686 self._mask, 

1687 skipna=skipna, 

1688 axis=axis, 

1689 ) 

1690 return self._wrap_reduction_result("max", result, skipna=skipna, axis=axis) 

1691 

1692 def map(self, mapper, na_action: Literal["ignore"] | None = None): 

1693 return map_array(self.to_numpy(), mapper, na_action=na_action) 

1694 

1695 @overload 

1696 def any( 

1697 self, *, skipna: Literal[True] = ..., axis: AxisInt | None = ..., **kwargs 

1698 ) -> np.bool_: ... 

1699 

1700 @overload 

1701 def any( 

1702 self, *, skipna: bool, axis: AxisInt | None = ..., **kwargs 

1703 ) -> np.bool_ | NAType: ... 

1704 

1705 def any( 

1706 self, *, skipna: bool = True, axis: AxisInt | None = 0, **kwargs 

1707 ) -> np.bool_ | NAType: 

1708 """ 

1709 Return whether any element is truthy. 

1710 

1711 Returns False unless there is at least one element that is truthy. 

1712 By default, NAs are skipped. If ``skipna=False`` is specified and 

1713 missing values are present, similar :ref:`Kleene logic <boolean.kleene>` 

1714 is used as for logical operations. 

1715 

1716 Parameters 

1717 ---------- 

1718 skipna : bool, default True 

1719 Exclude NA values. If the entire array is NA and `skipna` is 

1720 True, then the result will be False, as for an empty array. 

1721 If `skipna` is False, the result will still be True if there is 

1722 at least one element that is truthy, otherwise NA will be returned 

1723 if there are NA's present. 

1724 axis : int, optional, default 0 

1725 **kwargs : any, default None 

1726 Additional keywords have no effect but might be accepted for 

1727 compatibility with NumPy. 

1728 

1729 Returns 

1730 ------- 

1731 bool or :attr:`pandas.NA` 

1732 

1733 See Also 

1734 -------- 

1735 numpy.any : Numpy version of this method. 

1736 BaseMaskedArray.all : Return whether all elements are truthy. 

1737 

1738 Examples 

1739 -------- 

1740 The result indicates whether any element is truthy (and by default 

1741 skips NAs): 

1742 

1743 >>> pd.array([True, False, True]).any() 

1744 np.True_ 

1745 >>> pd.array([True, False, pd.NA]).any() 

1746 np.True_ 

1747 >>> pd.array([False, False, pd.NA]).any() 

1748 np.False_ 

1749 >>> pd.array([], dtype="boolean").any() 

1750 np.False_ 

1751 >>> pd.array([pd.NA], dtype="boolean").any() 

1752 np.False_ 

1753 >>> pd.array([pd.NA], dtype="Float64").any() 

1754 np.False_ 

1755 

1756 With ``skipna=False``, the result can be NA if this is logically 

1757 required (whether ``pd.NA`` is True or False influences the result): 

1758 

1759 >>> pd.array([True, False, pd.NA]).any(skipna=False) 

1760 np.True_ 

1761 >>> pd.array([1, 0, pd.NA]).any(skipna=False) 

1762 np.True_ 

1763 >>> pd.array([False, False, pd.NA]).any(skipna=False) 

1764 <NA> 

1765 >>> pd.array([0, 0, pd.NA]).any(skipna=False) 

1766 <NA> 

1767 """ 

1768 nv.validate_any((), kwargs) 

1769 

1770 values = self._data.copy() 

1771 np.putmask(values, self._mask, self.dtype._falsey_value) 

1772 result = values.any() 

1773 if skipna: 

1774 return result 

1775 elif result or len(self) == 0 or not self._mask.any(): 

1776 return result 

1777 else: 

1778 return self.dtype.na_value 

1779 

1780 @overload 

1781 def all( 

1782 self, *, skipna: Literal[True] = ..., axis: AxisInt | None = ..., **kwargs 

1783 ) -> np.bool_: ... 

1784 

1785 @overload 

1786 def all( 

1787 self, *, skipna: bool, axis: AxisInt | None = ..., **kwargs 

1788 ) -> np.bool_ | NAType: ... 

1789 

1790 def all( 

1791 self, *, skipna: bool = True, axis: AxisInt | None = 0, **kwargs 

1792 ) -> np.bool_ | NAType: 

1793 """ 

1794 Return whether all elements are truthy. 

1795 

1796 Returns True unless there is at least one element that is falsey. 

1797 By default, NAs are skipped. If ``skipna=False`` is specified and 

1798 missing values are present, similar :ref:`Kleene logic <boolean.kleene>` 

1799 is used as for logical operations. 

1800 

1801 Parameters 

1802 ---------- 

1803 skipna : bool, default True 

1804 Exclude NA values. If the entire array is NA and `skipna` is 

1805 True, then the result will be True, as for an empty array. 

1806 If `skipna` is False, the result will still be False if there is 

1807 at least one element that is falsey, otherwise NA will be returned 

1808 if there are NA's present. 

1809 axis : int, optional, default 0 

1810 **kwargs : any, default None 

1811 Additional keywords have no effect but might be accepted for 

1812 compatibility with NumPy. 

1813 

1814 Returns 

1815 ------- 

1816 bool or :attr:`pandas.NA` 

1817 

1818 See Also 

1819 -------- 

1820 numpy.all : Numpy version of this method. 

1821 BooleanArray.any : Return whether any element is truthy. 

1822 

1823 Examples 

1824 -------- 

1825 The result indicates whether all elements are truthy (and by default 

1826 skips NAs): 

1827 

1828 >>> pd.array([True, True, pd.NA]).all() 

1829 np.True_ 

1830 >>> pd.array([1, 1, pd.NA]).all() 

1831 np.True_ 

1832 >>> pd.array([True, False, pd.NA]).all() 

1833 np.False_ 

1834 >>> pd.array([], dtype="boolean").all() 

1835 np.True_ 

1836 >>> pd.array([pd.NA], dtype="boolean").all() 

1837 np.True_ 

1838 >>> pd.array([pd.NA], dtype="Float64").all() 

1839 np.True_ 

1840 

1841 With ``skipna=False``, the result can be NA if this is logically 

1842 required (whether ``pd.NA`` is True or False influences the result): 

1843 

1844 >>> pd.array([True, True, pd.NA]).all(skipna=False) 

1845 <NA> 

1846 >>> pd.array([1, 1, pd.NA]).all(skipna=False) 

1847 <NA> 

1848 >>> pd.array([True, False, pd.NA]).all(skipna=False) 

1849 np.False_ 

1850 >>> pd.array([1, 0, pd.NA]).all(skipna=False) 

1851 np.False_ 

1852 """ 

1853 nv.validate_all((), kwargs) 

1854 

1855 values = self._data.copy() 

1856 np.putmask(values, self._mask, self.dtype._truthy_value) 

1857 result = values.all(axis=axis) 

1858 

1859 if skipna: 

1860 return result # type: ignore[return-value] 

1861 elif not result or len(self) == 0 or not self._mask.any(): 

1862 return result # type: ignore[return-value] 

1863 else: 

1864 return self.dtype.na_value 

1865 

1866 def interpolate( 

1867 self, 

1868 *, 

1869 method: InterpolateOptions, 

1870 axis: int, 

1871 index, 

1872 limit, 

1873 limit_direction, 

1874 limit_area, 

1875 copy: bool, 

1876 **kwargs, 

1877 ) -> FloatingArray: 

1878 """ 

1879 See NDFrame.interpolate.__doc__. 

1880 """ 

1881 # NB: we return type(self) even if copy=False 

1882 if self.dtype.kind == "f": 

1883 if copy: 

1884 data = self._data.copy() 

1885 mask = self._mask.copy() 

1886 else: 

1887 data = self._data 

1888 mask = self._mask 

1889 elif self.dtype.kind in "iu": 

1890 copy = True 

1891 data = self._data.astype("f8") 

1892 mask = self._mask.copy() 

1893 else: 

1894 raise NotImplementedError( 

1895 f"interpolate is not implemented for dtype={self.dtype}" 

1896 ) 

1897 

1898 missing.interpolate_2d_inplace( 

1899 data, 

1900 method=method, 

1901 axis=0, 

1902 index=index, 

1903 limit=limit, 

1904 limit_direction=limit_direction, 

1905 limit_area=limit_area, 

1906 mask=mask, 

1907 **kwargs, 

1908 ) 

1909 if not copy: 

1910 return self # type: ignore[return-value] 

1911 if self.dtype.kind == "f": 

1912 return type(self)._simple_new(data, mask) # type: ignore[return-value] 

1913 else: 

1914 from pandas.core.arrays import FloatingArray 

1915 

1916 return FloatingArray._simple_new(data, mask) 

1917 

1918 def _accumulate( 

1919 self, name: str, *, skipna: bool = True, **kwargs 

1920 ) -> BaseMaskedArray: 

1921 data = self._data 

1922 mask = self._mask 

1923 

1924 op = getattr(masked_accumulations, name) 

1925 data, mask = op(data, mask, skipna=skipna, **kwargs) 

1926 

1927 return self._simple_new(data, mask) 

1928 

1929 # ------------------------------------------------------------------ 

1930 # GroupBy Methods 

1931 

1932 def _groupby_op( 

1933 self, 

1934 *, 

1935 how: str, 

1936 has_dropped_na: bool, 

1937 min_count: int, 

1938 ngroups: int, 

1939 ids: npt.NDArray[np.intp], 

1940 **kwargs, 

1941 ): 

1942 from pandas.core.groupby.ops import WrappedCythonOp 

1943 

1944 kind = WrappedCythonOp.get_kind_from_how(how) 

1945 op = WrappedCythonOp(how=how, kind=kind, has_dropped_na=has_dropped_na) 

1946 

1947 # libgroupby functions are responsible for NOT altering mask 

1948 mask = self._mask 

1949 if op.kind != "aggregate": 

1950 result_mask = mask.copy() 

1951 else: 

1952 result_mask = np.zeros(ngroups, dtype=bool) 

1953 

1954 if how == "rank" and kwargs.get("na_option") in ["top", "bottom"]: 

1955 result_mask[:] = False 

1956 

1957 res_values = op._cython_op_ndim_compat( 

1958 self._data, 

1959 min_count=min_count, 

1960 ngroups=ngroups, 

1961 comp_ids=ids, 

1962 mask=mask, 

1963 result_mask=result_mask, 

1964 **kwargs, 

1965 ) 

1966 

1967 if op.how == "ohlc": 

1968 arity = op._cython_arity.get(op.how, 1) 

1969 result_mask = np.tile(result_mask, (arity, 1)).T 

1970 

1971 if op.how in ["idxmin", "idxmax"]: 

1972 # Result values are indexes to take, keep as ndarray 

1973 return res_values 

1974 else: 

1975 # res_values should already have the correct dtype, we just need to 

1976 # wrap in a MaskedArray 

1977 return self._maybe_mask_result(res_values, result_mask) 

1978 

1979 

1980def transpose_homogeneous_masked_arrays( 

1981 masked_arrays: Sequence[BaseMaskedArray], 

1982) -> list[BaseMaskedArray]: 

1983 """Transpose masked arrays in a list, but faster. 

1984 

1985 Input should be a list of 1-dim masked arrays of equal length and all have the 

1986 same dtype. The caller is responsible for ensuring validity of input data. 

1987 """ 

1988 masked_arrays = list(masked_arrays) 

1989 dtype = masked_arrays[0].dtype 

1990 

1991 values = [arr._data.reshape(1, -1) for arr in masked_arrays] 

1992 transposed_values = np.concatenate( 

1993 values, 

1994 axis=0, 

1995 out=np.empty( 

1996 (len(masked_arrays), len(masked_arrays[0])), 

1997 order="F", 

1998 dtype=dtype.numpy_dtype, 

1999 ), 

2000 ) 

2001 

2002 masks = [arr._mask.reshape(1, -1) for arr in masked_arrays] 

2003 transposed_masks = np.concatenate( 

2004 masks, axis=0, out=np.empty_like(transposed_values, dtype=bool) 

2005 ) 

2006 

2007 arr_type = dtype.construct_array_type() 

2008 transposed_arrays: list[BaseMaskedArray] = [] 

2009 for i in range(transposed_values.shape[1]): 

2010 transposed_arr = arr_type(transposed_values[:, i], mask=transposed_masks[:, i]) 

2011 transposed_arrays.append(transposed_arr) 

2012 

2013 return transposed_arrays