1"""
2SparseArray data structure
3"""
4
5from __future__ import annotations
6
7from collections import abc
8import numbers
9import operator
10from typing import (
11 TYPE_CHECKING,
12 Any,
13 Literal,
14 Self,
15 cast,
16 overload,
17)
18import warnings
19
20import numpy as np
21
22from pandas._config.config import get_option
23
24from pandas._libs import lib
25import pandas._libs.sparse as splib
26from pandas._libs.sparse import (
27 BlockIndex,
28 IntIndex,
29 SparseIndex,
30)
31from pandas._libs.tslibs import NaT
32from pandas.compat.numpy import function as nv
33from pandas.errors import PerformanceWarning
34from pandas.util._decorators import (
35 doc,
36 set_module,
37)
38from pandas.util._exceptions import find_stack_level
39from pandas.util._validators import (
40 validate_bool_kwarg,
41 validate_insert_loc,
42)
43
44from pandas.core.dtypes.astype import astype_array
45from pandas.core.dtypes.cast import (
46 find_common_type,
47 maybe_box_datetimelike,
48)
49from pandas.core.dtypes.common import (
50 is_bool_dtype,
51 is_integer,
52 is_list_like,
53 is_object_dtype,
54 is_scalar,
55 is_string_dtype,
56 pandas_dtype,
57)
58from pandas.core.dtypes.dtypes import (
59 DatetimeTZDtype,
60 SparseDtype,
61)
62from pandas.core.dtypes.generic import (
63 ABCIndex,
64 ABCSeries,
65)
66from pandas.core.dtypes.missing import (
67 isna,
68 na_value_for_dtype,
69 notna,
70)
71
72from pandas.core import arraylike
73import pandas.core.algorithms as algos
74from pandas.core.arraylike import OpsMixin
75from pandas.core.arrays import ExtensionArray
76from pandas.core.base import PandasObject
77import pandas.core.common as com
78from pandas.core.construction import (
79 ensure_wrapped_if_datetimelike,
80 extract_array,
81 sanitize_array,
82)
83from pandas.core.indexers import (
84 check_array_indexer,
85 unpack_tuple_and_ellipses,
86)
87from pandas.core.nanops import check_below_min_count
88
89from pandas.io.formats import printing
90
91if TYPE_CHECKING:
92 from collections.abc import (
93 Callable,
94 Sequence,
95 )
96 from types import EllipsisType
97 from typing import (
98 Protocol,
99 type_check_only,
100 )
101
102 from scipy.sparse import (
103 csc_array,
104 csc_matrix,
105 )
106
107 @type_check_only
108 class _SparseMatrixLike(Protocol):
109 @property
110 def shape(self, /) -> tuple[int, int]: ...
111 def tocsc(self, /) -> csc_array | csc_matrix: ...
112
113 from pandas._typing import NumpySorter
114
115 SparseIndexKind = Literal["integer", "block"]
116
117 from pandas._typing import (
118 ArrayLike,
119 AstypeArg,
120 Axis,
121 AxisInt,
122 Dtype,
123 NpDtype,
124 PositionalIndexer,
125 Scalar,
126 ScalarIndexer,
127 SequenceIndexer,
128 npt,
129 )
130
131 from pandas import Series
132
133
134# ----------------------------------------------------------------------------
135# Array
136
137_sparray_doc_kwargs = {"klass": "SparseArray"}
138
139
140def _get_fill(arr: SparseArray) -> np.ndarray:
141 """
142 Create a 0-dim ndarray containing the fill value
143
144 Parameters
145 ----------
146 arr : SparseArray
147
148 Returns
149 -------
150 fill_value : ndarray
151 0-dim ndarray with just the fill value.
152
153 Notes
154 -----
155 coerce fill_value to arr dtype if possible
156 int64 SparseArray can have NaN as fill_value if there is no missing
157 """
158 try:
159 return np.asarray(arr.fill_value, dtype=arr.dtype.subtype)
160 except ValueError:
161 return np.asarray(arr.fill_value)
162
163
164def _sparse_array_op(
165 left: SparseArray, right: SparseArray, op: Callable, name: str
166) -> SparseArray:
167 """
168 Perform a binary operation between two arrays.
169
170 Parameters
171 ----------
172 left : Union[SparseArray, ndarray]
173 right : Union[SparseArray, ndarray]
174 op : Callable
175 The binary operation to perform
176 name str
177 Name of the callable.
178
179 Returns
180 -------
181 SparseArray
182 """
183 if name.startswith("__"):
184 # For lookups in _libs.sparse we need non-dunder op name
185 name = name[2:-2]
186
187 # dtype used to find corresponding sparse method
188 ltype = left.dtype.subtype
189 rtype = right.dtype.subtype
190
191 if ltype != rtype:
192 subtype = find_common_type([ltype, rtype])
193 ltype = SparseDtype(subtype, left.fill_value)
194 rtype = SparseDtype(subtype, right.fill_value)
195
196 left = left.astype(ltype, copy=False)
197 right = right.astype(rtype, copy=False)
198 dtype = ltype.subtype
199 else:
200 dtype = ltype
201
202 # dtype the result must have
203 result_dtype = None
204
205 if left.sp_index.ngaps == 0 or right.sp_index.ngaps == 0:
206 with np.errstate(all="ignore"):
207 result = op(left.to_dense(), right.to_dense())
208 fill = op(_get_fill(left), _get_fill(right))
209
210 if left.sp_index.ngaps == 0:
211 index = left.sp_index
212 else:
213 index = right.sp_index
214 elif left.sp_index.equals(right.sp_index):
215 with np.errstate(all="ignore"):
216 result = op(left.sp_values, right.sp_values)
217 fill = op(_get_fill(left), _get_fill(right))
218 index = left.sp_index
219 else:
220 if name[0] == "r":
221 left, right = right, left
222 name = name[1:]
223
224 if name in ("and", "or", "xor") and dtype == "bool":
225 opname = f"sparse_{name}_uint8"
226 # to make template simple, cast here
227 left_sp_values = left.sp_values.view(np.uint8)
228 right_sp_values = right.sp_values.view(np.uint8)
229 result_dtype = bool
230 else:
231 opname = f"sparse_{name}_{dtype}"
232 left_sp_values = left.sp_values
233 right_sp_values = right.sp_values
234
235 if (
236 name in ["floordiv", "mod"]
237 and (right == 0).any()
238 and left.dtype.kind in "iu"
239 ):
240 # Match the non-Sparse Series behavior
241 opname = f"sparse_{name}_float64"
242 left_sp_values = left_sp_values.astype("float64")
243 right_sp_values = right_sp_values.astype("float64")
244
245 sparse_op = getattr(splib, opname)
246
247 with np.errstate(all="ignore"):
248 result, index, fill = sparse_op(
249 left_sp_values,
250 left.sp_index,
251 left.fill_value,
252 right_sp_values,
253 right.sp_index,
254 right.fill_value,
255 )
256
257 if name == "divmod":
258 # result is a 2-tuple
259 # error: Incompatible return value type (got "Tuple[SparseArray,
260 # SparseArray]", expected "SparseArray")
261 return ( # type: ignore[return-value]
262 _wrap_result(name, result[0], index, fill[0], dtype=result_dtype),
263 _wrap_result(name, result[1], index, fill[1], dtype=result_dtype),
264 )
265
266 if result_dtype is None:
267 result_dtype = result.dtype
268
269 return _wrap_result(name, result, index, fill, dtype=result_dtype)
270
271
272def _wrap_result(
273 name: str, data, sparse_index, fill_value, dtype: Dtype | None = None
274) -> SparseArray:
275 """
276 wrap op result to have correct dtype
277 """
278 if name.startswith("__"):
279 # e.g. __eq__ --> eq
280 name = name[2:-2]
281
282 if name in ("eq", "ne", "lt", "gt", "le", "ge"):
283 dtype = bool
284
285 fill_value = lib.item_from_zerodim(fill_value)
286
287 if is_bool_dtype(dtype):
288 # fill_value may be np.bool_
289 fill_value = bool(fill_value)
290 return SparseArray(
291 data, sparse_index=sparse_index, fill_value=fill_value, dtype=dtype
292 )
293
294
295@set_module("pandas.arrays")
296class SparseArray(OpsMixin, PandasObject, ExtensionArray):
297 """
298 An ExtensionArray for storing sparse data.
299
300 SparseArray efficiently stores data with a high frequency of a
301 specific fill value (e.g., zeros), saving memory by only retaining
302 non-fill elements and their indices. This class is particularly
303 useful for large datasets where most values are redundant.
304
305 Parameters
306 ----------
307 data : array-like or scalar
308 A dense array of values to store in the SparseArray. This may contain
309 `fill_value`.
310 sparse_index : SparseIndex, optional
311 Index indicating the locations of sparse elements.
312 fill_value : scalar, optional
313 Elements in data that are ``fill_value`` are not stored in the
314 SparseArray. For memory savings, this should be the most common value
315 in `data`. By default, `fill_value` depends on the dtype of `data`:
316
317 =========== ==========
318 data.dtype na_value
319 =========== ==========
320 float ``np.nan``
321 int ``0``
322 bool False
323 datetime64 ``pd.NaT``
324 timedelta64 ``pd.NaT``
325 =========== ==========
326
327 The fill value is potentially specified in three ways. In order of
328 precedence, these are
329
330 1. The `fill_value` argument
331 2. ``dtype.fill_value`` if `fill_value` is None and `dtype` is
332 a ``SparseDtype``
333 3. ``data.dtype.fill_value`` if `fill_value` is None and `dtype`
334 is not a ``SparseDtype`` and `data` is a ``SparseArray``.
335
336 kind : str
337 Can be 'integer' or 'block', default is 'integer'.
338 The type of storage for sparse locations.
339
340 * 'block': Stores a `block` and `block_length` for each
341 contiguous *span* of sparse values. This is best when
342 sparse data tends to be clumped together, with large
343 regions of ``fill-value`` values between sparse values.
344 * 'integer': uses an integer to store the location of
345 each sparse value.
346
347 dtype : np.dtype or SparseDtype, optional
348 The dtype to use for the SparseArray. For numpy dtypes, this
349 determines the dtype of ``self.sp_values``. For SparseDtype,
350 this determines ``self.sp_values`` and ``self.fill_value``.
351 copy : bool, default False
352 Whether to explicitly copy the incoming `data` array.
353
354 Attributes
355 ----------
356 None
357
358 Methods
359 -------
360 None
361
362 See Also
363 --------
364 SparseDtype : Dtype for sparse data.
365
366 Examples
367 --------
368 >>> from pandas.arrays import SparseArray
369 >>> arr = SparseArray([0, 0, 1, 2])
370 >>> arr
371 [0, 0, 1, 2]
372 Fill: 0
373 IntIndex
374 Indices: array([2, 3], dtype=int32)
375 """
376
377 _subtyp = "sparse_array" # register ABCSparseArray
378 _hidden_attrs = PandasObject._hidden_attrs | frozenset([])
379 _sparse_index: SparseIndex
380 _sparse_values: np.ndarray
381 _dtype: SparseDtype
382
383 def __init__(
384 self,
385 data,
386 sparse_index=None,
387 fill_value=None,
388 kind: SparseIndexKind = "integer",
389 dtype: Dtype | None = None,
390 copy: bool = False,
391 ) -> None:
392 if fill_value is None and isinstance(dtype, SparseDtype):
393 fill_value = dtype.fill_value
394
395 if isinstance(data, type(self)):
396 # disable normal inference on dtype, sparse_index, & fill_value
397 if sparse_index is None:
398 sparse_index = data.sp_index
399 if fill_value is None:
400 fill_value = data.fill_value
401 if dtype is None:
402 dtype = data.dtype
403 # TODO: make kind=None, and use data.kind?
404 data = data.sp_values
405
406 # Handle use-provided dtype
407 if isinstance(dtype, str):
408 # Two options: dtype='int', regular numpy dtype
409 # or dtype='Sparse[int]', a sparse dtype
410 try:
411 dtype = SparseDtype.construct_from_string(dtype)
412 except TypeError:
413 dtype = pandas_dtype(dtype)
414
415 if isinstance(dtype, SparseDtype):
416 if fill_value is None:
417 fill_value = dtype.fill_value
418 dtype = dtype.subtype
419
420 if is_scalar(data):
421 raise TypeError(
422 f"Cannot construct {type(self).__name__} from scalar data. "
423 "Pass a sequence instead."
424 )
425
426 if dtype is not None:
427 dtype = pandas_dtype(dtype)
428
429 # TODO: disentangle the fill_value dtype inference from
430 # dtype inference
431 if data is None:
432 # TODO: What should the empty dtype be? Object or float?
433
434 # error: Argument "dtype" to "array" has incompatible type
435 # "Union[ExtensionDtype, dtype[Any], None]"; expected "Union[dtype[Any],
436 # None, type, _SupportsDType, str, Union[Tuple[Any, int], Tuple[Any,
437 # Union[int, Sequence[int]]], List[Any], _DTypeDict, Tuple[Any, Any]]]"
438 data = np.array([], dtype=dtype) # type: ignore[arg-type]
439
440 try:
441 data = sanitize_array(data, index=None)
442 except ValueError:
443 # NumPy may raise a ValueError on data like [1, []]
444 # we retry with object dtype here.
445 if dtype is None:
446 dtype = np.dtype(object)
447 data = np.atleast_1d(np.asarray(data, dtype=dtype))
448 else:
449 raise
450
451 if copy:
452 # TODO: avoid double copy when dtype forces cast.
453 data = data.copy()
454
455 if fill_value is None:
456 fill_value_dtype = data.dtype if dtype is None else dtype
457 if fill_value_dtype is None:
458 fill_value = np.nan
459 else:
460 fill_value = na_value_for_dtype(fill_value_dtype)
461
462 if isinstance(data, type(self)) and sparse_index is None:
463 sparse_index = data._sparse_index
464 # error: Argument "dtype" to "asarray" has incompatible type
465 # "Union[ExtensionDtype, dtype[Any], None]"; expected "None"
466 sparse_values = np.asarray(
467 data.sp_values,
468 dtype=dtype, # type: ignore[arg-type]
469 )
470 elif sparse_index is None:
471 data = extract_array(data, extract_numpy=True)
472 if not isinstance(data, np.ndarray):
473 # EA
474 if isinstance(data.dtype, DatetimeTZDtype):
475 warnings.warn(
476 f"Creating SparseArray from {data.dtype} data "
477 "loses timezone information. Cast to object before "
478 "sparse to retain timezone information.",
479 UserWarning,
480 stacklevel=find_stack_level(),
481 )
482 data = np.asarray(data, dtype="datetime64[ns]")
483 if fill_value is NaT:
484 fill_value = np.datetime64("NaT", "ns")
485 data = np.asarray(data)
486 sparse_values, sparse_index, fill_value = _make_sparse(
487 # error: Argument "dtype" to "_make_sparse" has incompatible type
488 # "Union[ExtensionDtype, dtype[Any], None]"; expected
489 # "Optional[dtype[Any]]"
490 data,
491 kind=kind,
492 fill_value=fill_value,
493 dtype=dtype, # type: ignore[arg-type]
494 )
495 else:
496 # error: Argument "dtype" to "asarray" has incompatible type
497 # "Union[ExtensionDtype, dtype[Any], None]"; expected "None"
498 sparse_values = np.asarray(data, dtype=dtype) # type: ignore[arg-type]
499 if len(sparse_values) != sparse_index.npoints:
500 raise AssertionError(
501 f"Non array-like type {type(sparse_values)} must "
502 "have the same length as the index"
503 )
504 self._sparse_index = sparse_index
505 self._sparse_values = sparse_values
506 self._dtype = SparseDtype(sparse_values.dtype, fill_value)
507
508 @classmethod
509 def _simple_new(
510 cls,
511 sparse_array: np.ndarray,
512 sparse_index: SparseIndex,
513 dtype: SparseDtype,
514 ) -> Self:
515 new = object.__new__(cls)
516 new._sparse_index = sparse_index
517 new._sparse_values = sparse_array
518 new._dtype = dtype
519 return new
520
521 @classmethod
522 def from_spmatrix(cls, data: _SparseMatrixLike) -> Self:
523 """
524 Create a SparseArray from a scipy.sparse matrix.
525
526 Parameters
527 ----------
528 data : scipy.sparse.sp_matrix
529 This should be a SciPy sparse matrix where the size
530 of the second dimension is 1. In other words, a
531 sparse matrix with a single column.
532
533 Returns
534 -------
535 SparseArray
536
537 Examples
538 --------
539 >>> import scipy.sparse
540 >>> mat = scipy.sparse.coo_matrix((4, 1))
541 >>> pd.arrays.SparseArray.from_spmatrix(mat)
542 [0.0, 0.0, 0.0, 0.0]
543 Fill: 0.0
544 IntIndex
545 Indices: array([], dtype=int32)
546 """
547 length, ncol = data.shape
548
549 if ncol != 1:
550 raise ValueError(f"'data' must have a single column, not '{ncol}'")
551
552 # our sparse index classes require that the positions be strictly
553 # increasing. So we need to sort loc, and arr accordingly.
554 data_csc = data.tocsc()
555 data_csc.sort_indices()
556 arr = data_csc.data
557 idx = data_csc.indices
558
559 zero = np.array(0, dtype=arr.dtype).item()
560 dtype = SparseDtype(arr.dtype, zero)
561 index = IntIndex(length, idx)
562
563 return cls._simple_new(arr, index, dtype)
564
565 def __array__(
566 self, dtype: NpDtype | None = None, copy: bool | None = None
567 ) -> np.ndarray:
568 if self.sp_index.ngaps == 0:
569 # Compat for na dtype and int values.
570 if copy is True:
571 return np.array(self.sp_values)
572 else:
573 result = self.sp_values
574 if self._readonly:
575 result = result.view()
576 result.flags.writeable = False
577 return result
578
579 if copy is False:
580 raise ValueError(
581 "Unable to avoid copy while creating an array as requested."
582 )
583
584 fill_value = self.fill_value
585
586 if dtype is None:
587 # Can NumPy represent this type?
588 # If not, `np.result_type` will raise. We catch that
589 # and return object.
590 if self.sp_values.dtype.kind == "M":
591 # However, we *do* special-case the common case of
592 # a datetime64 with pandas NaT.
593 if fill_value is NaT:
594 # Can't put pd.NaT in a datetime64[ns]
595 unit = np.datetime_data(self.sp_values.dtype)[0]
596 fill_value = np.datetime64("NaT", unit) # type: ignore[call-overload]
597 try:
598 dtype = np.result_type(self.sp_values.dtype, type(fill_value))
599 except TypeError:
600 dtype = object
601
602 out = np.full(self.shape, fill_value, dtype=dtype)
603 out[self.sp_index.indices] = self.sp_values
604 return out
605
606 def __setitem__(self, key, value) -> None:
607 if self._readonly:
608 raise ValueError("Cannot modify read-only array")
609 # I suppose we could allow setting of non-fill_value elements.
610 # TODO(SparseArray.__setitem__): remove special cases in
611 # ExtensionBlock.where
612 msg = "SparseArray does not support item assignment via setitem"
613 raise TypeError(msg)
614
615 @classmethod
616 def _from_sequence(
617 cls, scalars, *, dtype: Dtype | None = None, copy: bool = False
618 ) -> Self:
619 return cls(scalars, dtype=dtype)
620
621 @classmethod
622 def _from_factorized(cls, values, original) -> Self:
623 return cls(values, dtype=original.dtype)
624
625 def _cast_pointwise_result(self, values):
626 values = np.asarray(values, dtype=object)
627 result = lib.maybe_convert_objects(values, convert_non_numeric=True)
628 if result.dtype.kind == self.dtype.kind:
629 try:
630 # e.g. test_groupby_agg_extension
631 res = type(self)._from_sequence(result, dtype=self.dtype)
632 if ((res == result) | (isna(result) & res.isna())).all():
633 # This does not hold for e.g.
634 # test_arith_frame_with_scalar[0-__truediv__]
635 return res
636 return type(self)._from_sequence(result)
637 except (ValueError, TypeError):
638 return type(self)._from_sequence(result)
639 else:
640 # e.g. test_combine_le avoid casting bools to Sparse[float64, nan]
641 return type(self)._from_sequence(result)
642
643 # ------------------------------------------------------------------------
644 # Data
645 # ------------------------------------------------------------------------
646 @property
647 def sp_index(self) -> SparseIndex:
648 """
649 The SparseIndex containing the location of non- ``fill_value`` points.
650 """
651 return self._sparse_index
652
653 @property
654 def sp_values(self) -> np.ndarray:
655 """
656 An ndarray containing the non- ``fill_value`` values.
657
658 This property returns the actual data values stored in the sparse
659 representation, excluding the values that are equal to the ``fill_value``.
660 The result is an ndarray of the underlying values, preserving the sparse
661 structure by omitting the default ``fill_value`` entries.
662
663 See Also
664 --------
665 Series.sparse.to_dense : Convert a Series from sparse values to dense.
666 Series.sparse.fill_value : Elements in `data` that are `fill_value` are
667 not stored.
668 Series.sparse.density : The percent of non- ``fill_value`` points, as decimal.
669
670 Examples
671 --------
672 >>> from pandas.arrays import SparseArray
673 >>> s = SparseArray([0, 0, 1, 0, 2], fill_value=0)
674 >>> s.sp_values
675 array([1, 2])
676 """
677 return self._sparse_values
678
679 @property
680 def dtype(self) -> SparseDtype:
681 return self._dtype
682
683 @property
684 def fill_value(self):
685 """
686 Elements in `data` that are `fill_value` are not stored.
687
688 For memory savings, this should be the most common value in the array.
689
690 See Also
691 --------
692 SparseDtype : Dtype for data stored in :class:`SparseArray`.
693 Series.value_counts : Return a Series containing counts of unique values.
694 Series.fillna : Fill NA/NaN in a Series with a specified value.
695
696 Examples
697 --------
698 >>> ser = pd.Series([0, 0, 2, 2, 2], dtype="Sparse[int]")
699 >>> ser.sparse.fill_value
700 0
701 >>> spa_dtype = pd.SparseDtype(dtype=np.int32, fill_value=2)
702 >>> ser = pd.Series([0, 0, 2, 2, 2], dtype=spa_dtype)
703 >>> ser.sparse.fill_value
704 2
705 """
706 return self.dtype.fill_value
707
708 @fill_value.setter
709 def fill_value(self, value) -> None:
710 self._dtype = SparseDtype(self.dtype.subtype, value)
711
712 @property
713 def kind(self) -> SparseIndexKind:
714 """
715 The kind of sparse index for this array. One of {'integer', 'block'}.
716 """
717 if isinstance(self.sp_index, IntIndex):
718 return "integer"
719 else:
720 return "block"
721
722 @property
723 def _valid_sp_values(self) -> np.ndarray:
724 sp_vals = self.sp_values
725 mask = notna(sp_vals)
726 return sp_vals[mask]
727
728 def __len__(self) -> int:
729 return self.sp_index.length
730
731 @property
732 def _null_fill_value(self) -> bool:
733 return self._dtype._is_na_fill_value
734
735 @property
736 def nbytes(self) -> int:
737 return self.sp_values.nbytes + self.sp_index.nbytes
738
739 @property
740 def density(self) -> float:
741 """
742 The percent of non- ``fill_value`` points, as decimal.
743
744 See Also
745 --------
746 DataFrame.sparse.from_spmatrix : Create a new DataFrame from a
747 scipy sparse matrix.
748
749 Examples
750 --------
751 >>> from pandas.arrays import SparseArray
752 >>> s = SparseArray([0, 0, 1, 1, 1], fill_value=0)
753 >>> s.density
754 0.6
755 """
756 return self.sp_index.npoints / self.sp_index.length
757
758 @property
759 def npoints(self) -> int:
760 """
761 The number of non- ``fill_value`` points.
762
763 This property returns the number of elements in the sparse series that are
764 not equal to the ``fill_value``. Sparse data structures store only the
765 non-``fill_value`` elements, reducing memory usage when the majority of
766 values are the same.
767
768 See Also
769 --------
770 Series.sparse.to_dense : Convert a Series from sparse values to dense.
771 Series.sparse.fill_value : Elements in ``data`` that are ``fill_value`` are
772 not stored.
773 Series.sparse.density : The percent of non- ``fill_value`` points, as decimal.
774
775 Examples
776 --------
777 >>> from pandas.arrays import SparseArray
778 >>> s = SparseArray([0, 0, 1, 1, 1], fill_value=0)
779 >>> s.npoints
780 3
781 """
782 return self.sp_index.npoints
783
784 # error: Return type "SparseArray" of "isna" incompatible with return type
785 # "ndarray[Any, Any] | ExtensionArraySupportsAnyAll" in supertype "ExtensionArray"
786 def isna(self) -> Self: # type: ignore[override]
787 # If null fill value, we want SparseDtype[bool, true]
788 # to preserve the same memory usage.
789 dtype = SparseDtype(bool, self._null_fill_value)
790 if self._null_fill_value:
791 return type(self)._simple_new(isna(self.sp_values), self.sp_index, dtype)
792 mask = np.full(len(self), False, dtype=np.bool_)
793 mask[self.sp_index.indices] = isna(self.sp_values)
794 return type(self)(mask, fill_value=False, dtype=dtype)
795
796 def fillna(
797 self,
798 value,
799 limit: int | None = None,
800 copy: bool = True,
801 ) -> Self:
802 """
803 Fill missing values with `value`.
804
805 Parameters
806 ----------
807 value : scalar
808 limit : int, optional
809 Not supported for SparseArray, must be None.
810 copy: bool, default True
811 Ignored for SparseArray.
812
813 Returns
814 -------
815 SparseArray
816
817 Notes
818 -----
819 When `value` is specified, the result's ``fill_value`` depends on
820 ``self.fill_value``. The goal is to maintain low-memory use.
821
822 If ``self.fill_value`` is NA, the result dtype will be
823 ``SparseDtype(self.dtype, fill_value=value)``. This will preserve
824 amount of memory used before and after filling.
825
826 When ``self.fill_value`` is not NA, the result dtype will be
827 ``self.dtype``. Again, this preserves the amount of memory used.
828 """
829 if limit is not None:
830 raise ValueError("limit must be None")
831 new_values = np.where(isna(self.sp_values), value, self.sp_values)
832
833 if self._null_fill_value:
834 # This is essentially just updating the dtype.
835 new_dtype = SparseDtype(self.dtype.subtype, fill_value=value)
836 else:
837 new_dtype = self.dtype
838
839 return self._simple_new(new_values, self._sparse_index, new_dtype)
840
841 def shift(self, periods: int = 1, fill_value=None) -> Self:
842 if not len(self) or periods == 0:
843 return self.copy()
844
845 if isna(fill_value):
846 fill_value = self.dtype.na_value
847
848 subtype = np.result_type(fill_value, self.dtype.subtype)
849
850 if subtype != self.dtype.subtype:
851 # just coerce up front
852 arr = self.astype(SparseDtype(subtype, self.fill_value))
853 else:
854 arr = self
855
856 empty = self._from_sequence(
857 [fill_value] * min(abs(periods), len(self)), dtype=arr.dtype
858 )
859
860 if periods > 0:
861 a = empty
862 b = arr[:-periods]
863 else:
864 a = arr[abs(periods) :]
865 b = empty
866 return arr._concat_same_type([a, b])
867
868 def _first_fill_value_loc(self):
869 """
870 Get the location of the first fill value.
871
872 Returns
873 -------
874 int
875 """
876 if len(self) == 0 or self.sp_index.npoints == len(self):
877 return -1
878
879 indices = self.sp_index.indices
880 if not len(indices) or indices[0] > 0:
881 return 0
882
883 # a number larger than 1 should be appended to
884 # the last in case of fill value only appears
885 # in the tail of array
886 diff = np.r_[np.diff(indices), 2]
887 return indices[(diff > 1).argmax()] + 1
888
889 @doc(ExtensionArray.duplicated)
890 def duplicated(
891 self, keep: Literal["first", "last", False] = "first"
892 ) -> npt.NDArray[np.bool_]:
893 values = np.asarray(self)
894 mask = np.asarray(self.isna())
895 return algos.duplicated(values, keep=keep, mask=mask)
896
897 def unique(self) -> Self:
898 uniques = algos.unique(self.sp_values)
899 if len(self.sp_values) != len(self):
900 fill_loc = self._first_fill_value_loc()
901 # Inorder to align the behavior of pd.unique or
902 # pd.Series.unique, we should keep the original
903 # order, here we use unique again to find the
904 # insertion place. Since the length of sp_values
905 # is not large, maybe minor performance hurt
906 # is worthwhile to the correctness.
907 insert_loc = len(algos.unique(self.sp_values[:fill_loc]))
908 uniques = np.insert(uniques, insert_loc, self.fill_value)
909 return type(self)._from_sequence(uniques, dtype=self.dtype)
910
911 def _values_for_factorize(self):
912 # Still override this for hash_pandas_object
913 return np.asarray(self), self.fill_value
914
915 def factorize(
916 self,
917 use_na_sentinel: bool = True,
918 ) -> tuple[np.ndarray, SparseArray]:
919 # Currently, ExtensionArray.factorize -> Tuple[ndarray, EA]
920 # The sparsity on this is backwards from what Sparse would want. Want
921 # ExtensionArray.factorize -> Tuple[EA, EA]
922 # Given that we have to return a dense array of codes, why bother
923 # implementing an efficient factorize?
924 codes, uniques = algos.factorize(
925 np.asarray(self), use_na_sentinel=use_na_sentinel
926 )
927 uniques_sp = SparseArray(uniques, dtype=self.dtype)
928 return codes, uniques_sp
929
930 def value_counts(self, dropna: bool = True) -> Series:
931 """
932 Returns a Series containing counts of unique values.
933
934 Parameters
935 ----------
936 dropna : bool, default True
937 Don't include counts of NaN, even if NaN is in sp_values.
938
939 Returns
940 -------
941 counts : Series
942 """
943 from pandas import (
944 Index,
945 Series,
946 )
947
948 keys, counts, _ = algos.value_counts_arraylike(self.sp_values, dropna=dropna)
949 fcounts = self.sp_index.ngaps
950 if fcounts > 0 and (not self._null_fill_value or not dropna):
951 mask = isna(keys) if self._null_fill_value else keys == self.fill_value
952 if mask.any():
953 counts[mask] += fcounts
954 else:
955 # error: Argument 1 to "insert" has incompatible type "Union[
956 # ExtensionArray,ndarray[Any, Any]]"; expected "Union[
957 # _SupportsArray[dtype[Any]], Sequence[_SupportsArray[dtype
958 # [Any]]], Sequence[Sequence[_SupportsArray[dtype[Any]]]],
959 # Sequence[Sequence[Sequence[_SupportsArray[dtype[Any]]]]], Sequence
960 # [Sequence[Sequence[Sequence[_SupportsArray[dtype[Any]]]]]]]"
961 keys = np.insert(keys, 0, self.fill_value) # type: ignore[arg-type]
962 counts = np.insert(counts, 0, fcounts)
963
964 if not isinstance(keys, ABCIndex):
965 index = Index(keys, copy=False)
966 else:
967 index = keys
968 return Series(counts, index=index, copy=False)
969
970 # --------
971 # Indexing
972 # --------
973 @overload
974 def __getitem__(self, key: ScalarIndexer) -> Any: ...
975
976 @overload
977 def __getitem__(
978 self,
979 key: SequenceIndexer | tuple[int | EllipsisType, ...],
980 ) -> Self: ...
981
982 def __getitem__(
983 self,
984 key: PositionalIndexer | tuple[int | EllipsisType, ...],
985 ) -> Self | Any:
986 if isinstance(key, tuple):
987 key = unpack_tuple_and_ellipses(key)
988 if key is ...:
989 raise ValueError("Cannot slice with Ellipsis")
990
991 if is_integer(key):
992 return self._get_val_at(key)
993 elif isinstance(key, tuple):
994 data_slice = self.to_dense()[key]
995 elif isinstance(key, slice):
996 if key == slice(None):
997 # to ensure arr[:] (used by view()) does not make a copy
998 result = type(self)._simple_new(
999 self.sp_values, self.sp_index, self.dtype
1000 )
1001 result._readonly = self._readonly
1002 return result
1003 # Avoid densifying when handling contiguous slices
1004 if key.step is None or key.step == 1:
1005 start = 0 if key.start is None else key.start
1006 if start < 0:
1007 start += len(self)
1008
1009 end = len(self) if key.stop is None else key.stop
1010 if end < 0:
1011 end += len(self)
1012
1013 indices = self.sp_index.indices
1014 keep_inds = np.flatnonzero((indices >= start) & (indices < end))
1015 sp_vals = self.sp_values[keep_inds]
1016
1017 sp_index = indices[keep_inds].copy()
1018
1019 # If we've sliced to not include the start of the array, all our indices
1020 # should be shifted. NB: here we are careful to also not shift by a
1021 # negative value for a case like [0, 1][-100:] where the start index
1022 # should be treated like 0
1023 if start > 0:
1024 sp_index -= start
1025
1026 # Length of our result should match applying this slice to a range
1027 # of the length of our original array
1028 new_len = len(range(len(self))[key])
1029 new_sp_index = make_sparse_index(new_len, sp_index, self.kind)
1030 return type(self)._simple_new(sp_vals, new_sp_index, self.dtype)
1031 else:
1032 indices = np.arange(len(self), dtype=np.int32)[key]
1033 return self.take(indices)
1034
1035 elif not is_list_like(key):
1036 # e.g. "foo" or 2.5
1037 # exception message copied from numpy
1038 raise IndexError(
1039 r"only integers, slices (`:`), ellipsis (`...`), numpy.newaxis "
1040 r"(`None`) and integer or boolean arrays are valid indices"
1041 )
1042
1043 else:
1044 if isinstance(key, SparseArray):
1045 # NOTE: If we guarantee that SparseDType(bool)
1046 # has only fill_value - true, false or nan
1047 # (see GH PR 44955)
1048 # we can apply mask very fast:
1049 if is_bool_dtype(key):
1050 if isna(key.fill_value):
1051 return self.take(key.sp_index.indices[key.sp_values])
1052 if not key.fill_value:
1053 return self.take(key.sp_index.indices)
1054 n = len(self)
1055 mask = np.full(n, True, dtype=np.bool_)
1056 mask[key.sp_index.indices] = False
1057 return self.take(np.arange(n)[mask])
1058 else:
1059 key = np.asarray(key)
1060
1061 key = check_array_indexer(self, key)
1062
1063 if com.is_bool_indexer(key):
1064 # mypy doesn't know we have an array here
1065 key = cast(np.ndarray, key)
1066 return self.take(np.arange(len(key), dtype=np.int32)[key])
1067 elif hasattr(key, "__len__"):
1068 return self.take(key)
1069 else:
1070 raise ValueError(f"Cannot slice with '{key}'")
1071
1072 return type(self)(data_slice, kind=self.kind)
1073
1074 def _get_val_at(self, loc):
1075 loc = validate_insert_loc(loc, len(self))
1076
1077 sp_loc = self.sp_index.lookup(loc)
1078 if sp_loc == -1:
1079 return self.fill_value
1080 else:
1081 val = self.sp_values[sp_loc]
1082 val = maybe_box_datetimelike(val, self.sp_values.dtype)
1083 return val
1084
1085 def take(self, indices, *, allow_fill: bool = False, fill_value=None) -> Self:
1086 if is_scalar(indices):
1087 raise ValueError(f"'indices' must be an array, not a scalar '{indices}'.")
1088 indices = np.asarray(indices, dtype=np.int32)
1089
1090 dtype = None
1091 if indices.size == 0:
1092 result = np.array([], dtype="object")
1093 dtype = self.dtype
1094 elif allow_fill:
1095 result = self._take_with_fill(indices, fill_value=fill_value)
1096 else:
1097 return self._take_without_fill(indices)
1098
1099 return type(self)(
1100 result, fill_value=self.fill_value, kind=self.kind, dtype=dtype
1101 )
1102
1103 def _take_with_fill(self, indices, fill_value=None) -> np.ndarray:
1104 if fill_value is None:
1105 fill_value = self.dtype.na_value
1106
1107 if indices.min() < -1:
1108 raise ValueError(
1109 "Invalid value in 'indices'. Must be between -1 "
1110 "and the length of the array."
1111 )
1112
1113 if indices.max() >= len(self):
1114 raise IndexError("out of bounds value in 'indices'.")
1115
1116 if len(self) == 0:
1117 # Empty... Allow taking only if all empty
1118 if (indices == -1).all():
1119 dtype = np.result_type(self.sp_values, type(fill_value))
1120 taken = np.empty_like(indices, dtype=dtype)
1121 taken.fill(fill_value)
1122 return taken
1123 else:
1124 raise IndexError("cannot do a non-empty take from an empty axes.")
1125
1126 # sp_indexer may be -1 for two reasons
1127 # 1.) we took for an index of -1 (new)
1128 # 2.) we took a value that was self.fill_value (old)
1129 sp_indexer = self.sp_index.lookup_array(indices)
1130 new_fill_indices = indices == -1
1131 old_fill_indices = (sp_indexer == -1) & ~new_fill_indices
1132
1133 if self.sp_index.npoints == 0 and old_fill_indices.all():
1134 # We've looked up all valid points on an all-sparse array.
1135 taken = np.full(
1136 sp_indexer.shape, fill_value=self.fill_value, dtype=self.dtype.subtype
1137 )
1138
1139 elif self.sp_index.npoints == 0:
1140 # Use the old fill_value unless we took for an index of -1
1141 _dtype = np.result_type(self.dtype.subtype, type(fill_value))
1142 taken = np.full(sp_indexer.shape, fill_value=fill_value, dtype=_dtype)
1143 taken[old_fill_indices] = self.fill_value
1144 else:
1145 taken = self.sp_values.take(sp_indexer)
1146
1147 # Fill in two steps.
1148 # Old fill values
1149 # New fill values
1150 # potentially coercing to a new dtype at each stage.
1151
1152 m0 = sp_indexer[old_fill_indices] < 0
1153 m1 = sp_indexer[new_fill_indices] < 0
1154
1155 result_type = taken.dtype
1156
1157 if m0.any():
1158 result_type = np.result_type(result_type, type(self.fill_value))
1159 taken = taken.astype(result_type)
1160 taken[old_fill_indices] = self.fill_value
1161
1162 if m1.any():
1163 result_type = np.result_type(result_type, type(fill_value))
1164 taken = taken.astype(result_type)
1165 taken[new_fill_indices] = fill_value
1166
1167 return taken
1168
1169 def _take_without_fill(self, indices) -> Self:
1170 to_shift = indices < 0
1171
1172 n = len(self)
1173
1174 if (indices.max() >= n) or (indices.min() < -n):
1175 if n == 0:
1176 raise IndexError("cannot do a non-empty take from an empty axes.")
1177 raise IndexError("out of bounds value in 'indices'.")
1178
1179 if to_shift.any():
1180 indices = indices.copy()
1181 indices[to_shift] += n
1182
1183 sp_indexer = self.sp_index.lookup_array(indices)
1184 value_mask = sp_indexer != -1
1185 new_sp_values = self.sp_values[sp_indexer[value_mask]]
1186
1187 value_indices = np.flatnonzero(value_mask).astype(np.int32, copy=False)
1188
1189 new_sp_index = make_sparse_index(len(indices), value_indices, kind=self.kind)
1190 return type(self)._simple_new(new_sp_values, new_sp_index, dtype=self.dtype)
1191
1192 def searchsorted(
1193 self,
1194 v: ArrayLike | object,
1195 side: Literal["left", "right"] = "left",
1196 sorter: NumpySorter | None = None,
1197 ) -> npt.NDArray[np.intp] | np.intp:
1198 if get_option("performance_warnings"):
1199 msg = "searchsorted requires high memory usage."
1200 warnings.warn(msg, PerformanceWarning, stacklevel=find_stack_level())
1201 v = np.asarray(v)
1202 return np.asarray(self, dtype=self.dtype.subtype).searchsorted(v, side, sorter)
1203
1204 def copy(self) -> Self:
1205 values = self.sp_values.copy()
1206 return self._simple_new(values, self.sp_index, self.dtype)
1207
1208 @classmethod
1209 def _concat_same_type(cls, to_concat: Sequence[Self]) -> Self:
1210 fill_value = to_concat[0].fill_value
1211
1212 values = []
1213 length = 0
1214
1215 if to_concat:
1216 sp_kind = to_concat[0].kind
1217 else:
1218 sp_kind = "integer"
1219
1220 sp_index: SparseIndex
1221 if sp_kind == "integer":
1222 indices = []
1223
1224 for arr in to_concat:
1225 int_idx = arr.sp_index.indices.copy()
1226 int_idx += length # TODO: wraparound
1227 length += arr.sp_index.length
1228
1229 values.append(arr.sp_values)
1230 indices.append(int_idx)
1231
1232 data = np.concatenate(values)
1233 indices_arr = np.concatenate(indices)
1234 sp_index = IntIndex(length, indices_arr)
1235
1236 else:
1237 # when concatenating block indices, we don't claim that you'll
1238 # get an identical index as concatenating the values and then
1239 # creating a new index. We don't want to spend the time trying
1240 # to merge blocks across arrays in `to_concat`, so the resulting
1241 # BlockIndex may have more blocks.
1242 blengths = []
1243 blocs = []
1244
1245 for arr in to_concat:
1246 block_idx = arr.sp_index.to_block_index()
1247
1248 values.append(arr.sp_values)
1249 blocs.append(block_idx.blocs.copy() + length)
1250 blengths.append(block_idx.blengths)
1251 length += arr.sp_index.length
1252
1253 data = np.concatenate(values)
1254 blocs_arr = np.concatenate(blocs)
1255 blengths_arr = np.concatenate(blengths)
1256
1257 sp_index = BlockIndex(length, blocs_arr, blengths_arr)
1258
1259 return cls(data, sparse_index=sp_index, fill_value=fill_value)
1260
1261 def astype(self, dtype: AstypeArg | None = None, copy: bool = True):
1262 """
1263 Change the dtype of a SparseArray.
1264
1265 The output will always be a SparseArray. To convert to a dense
1266 ndarray with a certain dtype, use :meth:`numpy.asarray`.
1267
1268 Parameters
1269 ----------
1270 dtype : np.dtype or ExtensionDtype
1271 For SparseDtype, this changes the dtype of
1272 ``self.sp_values`` and the ``self.fill_value``.
1273
1274 For other dtypes, this only changes the dtype of
1275 ``self.sp_values``.
1276
1277 copy : bool, default True
1278 Whether to ensure a copy is made, even if not necessary.
1279
1280 Returns
1281 -------
1282 SparseArray
1283
1284 Examples
1285 --------
1286 >>> arr = pd.arrays.SparseArray([0, 0, 1, 2])
1287 >>> arr
1288 [0, 0, 1, 2]
1289 Fill: 0
1290 IntIndex
1291 Indices: array([2, 3], dtype=int32)
1292
1293 >>> arr.astype(pd.SparseDtype(np.dtype("int32")))
1294 [0, 0, 1, 2]
1295 Fill: 0
1296 IntIndex
1297 Indices: array([2, 3], dtype=int32)
1298
1299 Using a NumPy dtype with a different kind (e.g. float) will coerce
1300 just ``self.sp_values``.
1301
1302 >>> arr.astype(pd.SparseDtype(np.dtype("float64")))
1303 ... # doctest: +NORMALIZE_WHITESPACE
1304 [nan, nan, 1.0, 2.0]
1305 Fill: nan
1306 IntIndex
1307 Indices: array([2, 3], dtype=int32)
1308
1309 Using a SparseDtype, you can also change the fill value as well.
1310
1311 >>> arr.astype(pd.SparseDtype("float64", fill_value=0.0))
1312 ... # doctest: +NORMALIZE_WHITESPACE
1313 [0.0, 0.0, 1.0, 2.0]
1314 Fill: 0.0
1315 IntIndex
1316 Indices: array([2, 3], dtype=int32)
1317 """
1318 if dtype == self._dtype:
1319 if not copy:
1320 return self
1321 else:
1322 return self.copy()
1323
1324 future_dtype = pandas_dtype(dtype)
1325 if not isinstance(future_dtype, SparseDtype):
1326 # GH#34457
1327 values = np.asarray(self)
1328 values = ensure_wrapped_if_datetimelike(values)
1329 return astype_array(values, dtype=future_dtype, copy=False)
1330
1331 dtype = self.dtype.update_dtype(dtype)
1332 subtype = pandas_dtype(dtype._subtype_with_str)
1333 subtype = cast(np.dtype, subtype) # ensured by update_dtype
1334 values = ensure_wrapped_if_datetimelike(self.sp_values)
1335 sp_values = astype_array(values, subtype, copy=copy)
1336 sp_values = np.asarray(sp_values)
1337
1338 return self._simple_new(sp_values, self.sp_index, dtype)
1339
1340 def map(self, mapper, na_action: Literal["ignore"] | None = None) -> Self:
1341 """
1342 Map categories using an input mapping or function.
1343
1344 Parameters
1345 ----------
1346 mapper : dict, Series, callable
1347 The correspondence from old values to new.
1348 na_action : {None, 'ignore'}, default None
1349 If 'ignore', propagate NA values, without passing them to the
1350 mapping correspondence.
1351
1352 Returns
1353 -------
1354 SparseArray
1355 The output array will have the same density as the input.
1356 The output fill value will be the result of applying the
1357 mapping to ``self.fill_value``
1358
1359 Examples
1360 --------
1361 >>> arr = pd.arrays.SparseArray([0, 1, 2])
1362 >>> arr.map(lambda x: x + 10)
1363 [10, 11, 12]
1364 Fill: 10
1365 IntIndex
1366 Indices: array([1, 2], dtype=int32)
1367
1368 >>> arr.map({0: 10, 1: 11, 2: 12})
1369 [10, 11, 12]
1370 Fill: 10
1371 IntIndex
1372 Indices: array([1, 2], dtype=int32)
1373
1374 >>> arr.map(pd.Series([10, 11, 12], index=[0, 1, 2]))
1375 [10, 11, 12]
1376 Fill: 10
1377 IntIndex
1378 Indices: array([1, 2], dtype=int32)
1379 """
1380 is_map = isinstance(mapper, (abc.Mapping, ABCSeries))
1381
1382 fill_val = self.fill_value
1383
1384 if na_action is None or notna(fill_val):
1385 fill_val = mapper.get(fill_val, fill_val) if is_map else mapper(fill_val)
1386
1387 def func(sp_val):
1388 new_sp_val = mapper.get(sp_val, None) if is_map else mapper(sp_val)
1389 # check identity and equality because nans are not equal to each other
1390 if new_sp_val is fill_val or new_sp_val == fill_val:
1391 msg = "fill value in the sparse values not supported"
1392 raise ValueError(msg)
1393 return new_sp_val
1394
1395 sp_values = [func(x) for x in self.sp_values]
1396
1397 return type(self)(sp_values, sparse_index=self.sp_index, fill_value=fill_val)
1398
1399 def to_dense(self) -> np.ndarray:
1400 """
1401 Convert SparseArray to a NumPy array.
1402
1403 Returns
1404 -------
1405 arr : NumPy array
1406 """
1407 return np.asarray(self, dtype=self.sp_values.dtype)
1408
1409 def _where(self, mask, value):
1410 # NB: may not preserve dtype, e.g. result may be Sparse[float64]
1411 # while self is Sparse[int64]
1412 naive_implementation = np.where(mask, self, value)
1413 dtype = SparseDtype(naive_implementation.dtype, fill_value=self.fill_value)
1414 result = type(self)._from_sequence(naive_implementation, dtype=dtype)
1415 return result
1416
1417 # ------------------------------------------------------------------------
1418 # IO
1419 # ------------------------------------------------------------------------
1420 def __setstate__(self, state) -> None:
1421 """Necessary for making this object picklable"""
1422 if isinstance(state, tuple):
1423 # Compat for pandas < 0.24.0
1424 nd_state, (fill_value, sp_index) = state
1425 sparse_values = np.array([])
1426 sparse_values.__setstate__(nd_state)
1427
1428 self._sparse_values = sparse_values
1429 self._sparse_index = sp_index
1430 self._dtype = SparseDtype(sparse_values.dtype, fill_value)
1431 else:
1432 self.__dict__.update(state)
1433
1434 def nonzero(self) -> tuple[npt.NDArray[np.int32]]:
1435 if self.fill_value == 0:
1436 return (self.sp_index.indices,)
1437 else:
1438 return (self.sp_index.indices[self.sp_values != 0],)
1439
1440 # ------------------------------------------------------------------------
1441 # Reductions
1442 # ------------------------------------------------------------------------
1443
1444 def _reduce(
1445 self, name: str, *, skipna: bool = True, keepdims: bool = False, **kwargs
1446 ):
1447 method = getattr(self, name, None)
1448
1449 if method is None:
1450 raise TypeError(f"cannot perform {name} with type {self.dtype}")
1451
1452 if skipna:
1453 arr = self
1454 else:
1455 arr = self.dropna()
1456
1457 result = getattr(arr, name)(**kwargs)
1458
1459 if keepdims:
1460 return type(self)([result], dtype=self.dtype)
1461 else:
1462 return result
1463
1464 def all(self, axis=None, *args, **kwargs):
1465 """
1466 Tests whether all elements evaluate True
1467
1468 Returns
1469 -------
1470 all : bool
1471
1472 See Also
1473 --------
1474 numpy.all
1475 """
1476 nv.validate_all(args, kwargs)
1477
1478 values = self.sp_values
1479
1480 if len(values) != len(self) and not np.all(self.fill_value):
1481 return False
1482
1483 return values.all()
1484
1485 def any(self, axis: AxisInt = 0, *args, **kwargs) -> bool:
1486 """
1487 Tests whether at least one of elements evaluate True
1488
1489 Returns
1490 -------
1491 any : bool
1492
1493 See Also
1494 --------
1495 numpy.any
1496 """
1497 nv.validate_any(args, kwargs)
1498
1499 values = self.sp_values
1500
1501 if len(values) != len(self) and np.any(self.fill_value):
1502 return True
1503
1504 return values.any().item()
1505
1506 def sum(
1507 self,
1508 axis: AxisInt = 0,
1509 min_count: int = 0,
1510 skipna: bool = True,
1511 *args,
1512 **kwargs,
1513 ) -> Scalar:
1514 """
1515 Sum of non-NA/null values
1516
1517 Parameters
1518 ----------
1519 axis : int, default 0
1520 Not Used. NumPy compatibility.
1521 min_count : int, default 0
1522 The required number of valid values to perform the summation. If fewer
1523 than ``min_count`` valid values are present, the result will be the missing
1524 value indicator for subarray type.
1525 *args, **kwargs
1526 Not Used. NumPy compatibility.
1527
1528 Returns
1529 -------
1530 scalar
1531 """
1532 nv.validate_sum(args, kwargs)
1533 valid_vals = self._valid_sp_values
1534 sp_sum = valid_vals.sum()
1535 has_na = self.sp_index.ngaps > 0 and not self._null_fill_value
1536
1537 if has_na and not skipna:
1538 return na_value_for_dtype(self.dtype.subtype, compat=False)
1539
1540 if self._null_fill_value:
1541 if check_below_min_count(valid_vals.shape, None, min_count):
1542 return na_value_for_dtype(self.dtype.subtype, compat=False)
1543 return sp_sum
1544 else:
1545 nsparse = self.sp_index.ngaps
1546 if check_below_min_count(valid_vals.shape, None, min_count - nsparse):
1547 return na_value_for_dtype(self.dtype.subtype, compat=False)
1548 return sp_sum + self.fill_value * nsparse
1549
1550 def cumsum(self, axis: AxisInt = 0, *args, **kwargs) -> SparseArray:
1551 """
1552 Cumulative sum of non-NA/null values.
1553
1554 When performing the cumulative summation, any non-NA/null values will
1555 be skipped. The resulting SparseArray will preserve the locations of
1556 NaN values, but the fill value will be `np.nan` regardless.
1557
1558 Parameters
1559 ----------
1560 axis : int or None
1561 Axis over which to perform the cumulative summation. If None,
1562 perform cumulative summation over flattened array.
1563
1564 Returns
1565 -------
1566 cumsum : SparseArray
1567 """
1568 nv.validate_cumsum(args, kwargs)
1569
1570 if axis is not None and axis >= self.ndim: # Mimic ndarray behaviour.
1571 raise ValueError(f"axis(={axis}) out of bounds")
1572
1573 if not self._null_fill_value:
1574 return SparseArray(self.to_dense(), fill_value=np.nan).cumsum()
1575
1576 return SparseArray(
1577 self.sp_values.cumsum(),
1578 sparse_index=self.sp_index,
1579 fill_value=self.fill_value,
1580 )
1581
1582 def mean(self, axis: Axis = 0, *args, **kwargs):
1583 """
1584 Mean of non-NA/null values
1585
1586 Returns
1587 -------
1588 mean : float
1589 """
1590 nv.validate_mean(args, kwargs)
1591 valid_vals = self._valid_sp_values
1592 sp_sum = valid_vals.sum()
1593 ct = len(valid_vals)
1594
1595 if self._null_fill_value:
1596 return sp_sum / ct
1597 else:
1598 nsparse = self.sp_index.ngaps
1599 return (sp_sum + self.fill_value * nsparse) / (ct + nsparse)
1600
1601 def max(self, *, axis: AxisInt | None = None, skipna: bool = True):
1602 """
1603 Max of array values, ignoring NA values if specified.
1604
1605 Parameters
1606 ----------
1607 axis : int, default 0
1608 Not Used. NumPy compatibility.
1609 skipna : bool, default True
1610 Whether to ignore NA values.
1611
1612 Returns
1613 -------
1614 scalar
1615 """
1616 nv.validate_minmax_axis(axis, self.ndim)
1617 return self._min_max("max", skipna=skipna)
1618
1619 def min(self, *, axis: AxisInt | None = None, skipna: bool = True):
1620 """
1621 Min of array values, ignoring NA values if specified.
1622
1623 Parameters
1624 ----------
1625 axis : int, default 0
1626 Not Used. NumPy compatibility.
1627 skipna : bool, default True
1628 Whether to ignore NA values.
1629
1630 Returns
1631 -------
1632 scalar
1633 """
1634 nv.validate_minmax_axis(axis, self.ndim)
1635 return self._min_max("min", skipna=skipna)
1636
1637 def _min_max(self, kind: Literal["min", "max"], skipna: bool) -> Scalar:
1638 """
1639 Min/max of non-NA/null values
1640
1641 Parameters
1642 ----------
1643 kind : {"min", "max"}
1644 skipna : bool
1645
1646 Returns
1647 -------
1648 scalar
1649 """
1650 valid_vals = self._valid_sp_values
1651 has_nonnull_fill_vals = not self._null_fill_value and self.sp_index.ngaps > 0
1652
1653 if len(valid_vals) > 0:
1654 sp_min_max = getattr(valid_vals, kind)()
1655
1656 # If a non-null fill value is currently present, it might be the min/max
1657 if has_nonnull_fill_vals:
1658 func = max if kind == "max" else min
1659 return func(sp_min_max, self.fill_value)
1660 elif skipna:
1661 return sp_min_max
1662 elif self.sp_index.ngaps == 0:
1663 # No NAs present
1664 return sp_min_max
1665 else:
1666 return na_value_for_dtype(self.dtype.subtype, compat=False)
1667 elif has_nonnull_fill_vals:
1668 return self.fill_value
1669 else:
1670 return na_value_for_dtype(self.dtype.subtype, compat=False)
1671
1672 def _argmin_argmax(self, kind: Literal["argmin", "argmax"]) -> int:
1673 values = self._sparse_values
1674 index = self._sparse_index.indices
1675 mask = np.asarray(isna(values))
1676 func = np.argmax if kind == "argmax" else np.argmin
1677
1678 idx = np.arange(values.shape[0])
1679 non_nans = values[~mask]
1680 non_nan_idx = idx[~mask]
1681
1682 _candidate = non_nan_idx[func(non_nans)]
1683 candidate = index[_candidate]
1684
1685 if isna(self.fill_value):
1686 return candidate
1687 if kind == "argmin" and self[candidate] < self.fill_value:
1688 return candidate
1689 if kind == "argmax" and self[candidate] > self.fill_value:
1690 return candidate
1691 _loc = self._first_fill_value_loc()
1692 if _loc == -1:
1693 # fill_value doesn't exist
1694 return candidate
1695 else:
1696 return _loc
1697
1698 def argmax(self, skipna: bool = True) -> int:
1699 validate_bool_kwarg(skipna, "skipna")
1700 if not skipna and self._hasna:
1701 raise ValueError("Encountered an NA value with skipna=False")
1702 return self._argmin_argmax("argmax")
1703
1704 def argmin(self, skipna: bool = True) -> int:
1705 validate_bool_kwarg(skipna, "skipna")
1706 if not skipna and self._hasna:
1707 raise ValueError("Encountered an NA value with skipna=False")
1708 return self._argmin_argmax("argmin")
1709
1710 # ------------------------------------------------------------------------
1711 # Ufuncs
1712 # ------------------------------------------------------------------------
1713
1714 _HANDLED_TYPES = (np.ndarray, numbers.Number)
1715
1716 def __array_ufunc__(self, ufunc: np.ufunc, method: str, *inputs, **kwargs):
1717 out = kwargs.get("out", ())
1718
1719 for x in inputs + out:
1720 if not isinstance(x, (*self._HANDLED_TYPES, SparseArray)):
1721 return NotImplemented
1722
1723 # for binary ops, use our custom dunder methods
1724 result = arraylike.maybe_dispatch_ufunc_to_dunder_op(
1725 self, ufunc, method, *inputs, **kwargs
1726 )
1727 if result is not NotImplemented:
1728 return result
1729
1730 if "out" in kwargs:
1731 # e.g. tests.arrays.sparse.test_arithmetics.test_ndarray_inplace
1732 res = arraylike.dispatch_ufunc_with_out(
1733 self, ufunc, method, *inputs, **kwargs
1734 )
1735 return res
1736
1737 if method == "reduce":
1738 result = arraylike.dispatch_reduction_ufunc(
1739 self, ufunc, method, *inputs, **kwargs
1740 )
1741 if result is not NotImplemented:
1742 # e.g. tests.series.test_ufunc.TestNumpyReductions
1743 return result
1744
1745 if len(inputs) == 1:
1746 # No alignment necessary.
1747 sp_values = getattr(ufunc, method)(self.sp_values, **kwargs)
1748 fill_value = getattr(ufunc, method)(self.fill_value, **kwargs)
1749
1750 if ufunc.nout > 1:
1751 # multiple outputs. e.g. modf
1752 arrays = tuple(
1753 self._simple_new(
1754 sp_value, self.sp_index, SparseDtype(sp_value.dtype, fv)
1755 )
1756 for sp_value, fv in zip(sp_values, fill_value, strict=True)
1757 )
1758 return arrays
1759 elif method == "reduce":
1760 # e.g. reductions
1761 return sp_values
1762
1763 return self._simple_new(
1764 sp_values, self.sp_index, SparseDtype(sp_values.dtype, fill_value)
1765 )
1766
1767 new_inputs = tuple(np.asarray(x) for x in inputs)
1768 result = getattr(ufunc, method)(*new_inputs, **kwargs)
1769 if out:
1770 if len(out) == 1:
1771 out = out[0]
1772 return out
1773
1774 if ufunc.nout > 1:
1775 return tuple(type(self)(x) for x in result)
1776 elif method == "at":
1777 # no return value
1778 return None
1779 else:
1780 return type(self)(result)
1781
1782 # ------------------------------------------------------------------------
1783 # Ops
1784 # ------------------------------------------------------------------------
1785
1786 def _arith_method(self, other, op):
1787 op_name = op.__name__
1788
1789 if isinstance(other, SparseArray):
1790 return _sparse_array_op(self, other, op, op_name)
1791
1792 elif is_scalar(other):
1793 with np.errstate(all="ignore"):
1794 fill = op(_get_fill(self), np.asarray(other))
1795 result = op(self.sp_values, other)
1796
1797 if op_name == "divmod":
1798 left, right = result
1799 lfill, rfill = fill
1800 return (
1801 _wrap_result(op_name, left, self.sp_index, lfill),
1802 _wrap_result(op_name, right, self.sp_index, rfill),
1803 )
1804
1805 return _wrap_result(op_name, result, self.sp_index, fill)
1806
1807 else:
1808 other = np.asarray(other)
1809 with np.errstate(all="ignore"):
1810 if len(self) != len(other):
1811 raise AssertionError(
1812 f"length mismatch: {len(self)} vs. {len(other)}"
1813 )
1814 if not isinstance(other, SparseArray):
1815 dtype = getattr(other, "dtype", None)
1816 other = SparseArray(other, fill_value=self.fill_value, dtype=dtype)
1817 return _sparse_array_op(self, other, op, op_name)
1818
1819 def _cmp_method(self, other, op) -> SparseArray:
1820 if not is_scalar(other) and not isinstance(other, type(self)):
1821 # convert list-like to ndarray
1822 other = np.asarray(other)
1823
1824 if isinstance(other, np.ndarray):
1825 # TODO: make this more flexible than just ndarray...
1826 other = SparseArray(other, fill_value=self.fill_value)
1827
1828 if isinstance(other, SparseArray):
1829 if len(self) != len(other):
1830 raise ValueError(
1831 f"operands have mismatched length {len(self)} and {len(other)}"
1832 )
1833
1834 op_name = op.__name__.strip("_")
1835 return _sparse_array_op(self, other, op, op_name)
1836 else:
1837 # scalar
1838 fill_value = op(self.fill_value, other)
1839 result = np.full(len(self), fill_value, dtype=np.bool_)
1840 result[self.sp_index.indices] = op(self.sp_values, other)
1841
1842 return type(self)(
1843 result,
1844 fill_value=fill_value,
1845 dtype=np.bool_,
1846 )
1847
1848 _logical_method = _cmp_method
1849
1850 def _unary_method(self, op) -> SparseArray:
1851 fill_value = op(np.array(self.fill_value)).item()
1852 dtype = SparseDtype(self.dtype.subtype, fill_value)
1853 # NOTE: if fill_value doesn't change
1854 # we just have to apply op to sp_values
1855 if isna(self.fill_value) or fill_value == self.fill_value:
1856 values = op(self.sp_values)
1857 return type(self)._simple_new(values, self.sp_index, self.dtype)
1858 # In the other case we have to recalc indexes
1859 return type(self)(op(self.to_dense()), dtype=dtype)
1860
1861 def __pos__(self) -> SparseArray:
1862 return self._unary_method(operator.pos)
1863
1864 def __neg__(self) -> SparseArray:
1865 return self._unary_method(operator.neg)
1866
1867 def __invert__(self) -> SparseArray:
1868 return self._unary_method(operator.invert)
1869
1870 def __abs__(self) -> SparseArray:
1871 return self._unary_method(operator.abs)
1872
1873 # ----------
1874 # Formatting
1875 # -----------
1876 def __repr__(self) -> str:
1877 pp_str = printing.pprint_thing(self)
1878 pp_fill = printing.pprint_thing(self.fill_value)
1879 pp_index = printing.pprint_thing(self.sp_index)
1880 return f"{pp_str}\nFill: {pp_fill}\n{pp_index}"
1881
1882 # error: Return type "None" of "_formatter" incompatible with return
1883 # type "Callable[[Any], str | None]" in supertype "ExtensionArray"
1884 def _formatter(self, boxed: bool = False) -> None: # type: ignore[override]
1885 # Defer to the formatter from the GenericArrayFormatter calling us.
1886 # This will infer the correct formatter from the dtype of the values.
1887 return None
1888
1889
1890def _make_sparse(
1891 arr: np.ndarray,
1892 kind: SparseIndexKind = "block",
1893 fill_value=None,
1894 dtype: np.dtype | None = None,
1895):
1896 """
1897 Convert ndarray to sparse format
1898
1899 Parameters
1900 ----------
1901 arr : ndarray
1902 kind : {'block', 'integer'}
1903 fill_value : NaN or another value
1904 dtype : np.dtype, optional
1905 copy : bool, default False
1906
1907 Returns
1908 -------
1909 (sparse_values, index, fill_value) : (ndarray, SparseIndex, Scalar)
1910 """
1911 assert isinstance(arr, np.ndarray)
1912
1913 if arr.ndim > 1:
1914 raise TypeError("expected dimension <= 1 data")
1915
1916 if fill_value is None:
1917 fill_value = na_value_for_dtype(arr.dtype)
1918
1919 if isna(fill_value):
1920 mask = notna(arr)
1921 else:
1922 # cast to object comparison to be safe
1923 if is_string_dtype(arr.dtype):
1924 arr = arr.astype(object)
1925
1926 if is_object_dtype(arr.dtype):
1927 # element-wise equality check method in numpy doesn't treat
1928 # each element type, eg. 0, 0.0, and False are treated as
1929 # same. So we have to check the both of its type and value.
1930 mask = splib.make_mask_object_ndarray(arr, fill_value)
1931 else:
1932 mask = arr != fill_value
1933
1934 length = len(arr)
1935 if length != len(mask):
1936 # the arr is a SparseArray
1937 indices = mask.sp_index.indices
1938 else:
1939 indices = mask.nonzero()[0].astype(np.int32)
1940
1941 index = make_sparse_index(length, indices, kind)
1942 sparsified_values = arr[mask]
1943 if dtype is not None:
1944 sparsified_values = ensure_wrapped_if_datetimelike(sparsified_values)
1945 sparsified_values = astype_array(sparsified_values, dtype=dtype)
1946 sparsified_values = np.asarray(sparsified_values)
1947
1948 # TODO: copy
1949 return sparsified_values, index, fill_value
1950
1951
1952@overload
1953def make_sparse_index(length: int, indices, kind: Literal["block"]) -> BlockIndex: ...
1954
1955
1956@overload
1957def make_sparse_index(length: int, indices, kind: Literal["integer"]) -> IntIndex: ...
1958
1959
1960def make_sparse_index(length: int, indices, kind: SparseIndexKind) -> SparseIndex:
1961 index: SparseIndex
1962 if kind == "block":
1963 locs, lens = splib.get_blocks(indices)
1964 index = BlockIndex(length, locs, lens)
1965 elif kind == "integer":
1966 index = IntIndex(length, indices)
1967 else: # pragma: no cover
1968 raise ValueError("must be block or integer type")
1969 return index