1from __future__ import annotations
2
3import numbers
4from typing import (
5 TYPE_CHECKING,
6 ClassVar,
7 Self,
8 cast,
9)
10
11import numpy as np
12
13from pandas._libs import (
14 lib,
15 missing as libmissing,
16)
17from pandas.util._decorators import set_module
18
19from pandas.core.dtypes.common import is_list_like
20from pandas.core.dtypes.dtypes import register_extension_dtype
21from pandas.core.dtypes.missing import isna
22
23from pandas.core import ops
24from pandas.core.array_algos import masked_accumulations
25from pandas.core.arrays.masked import (
26 BaseMaskedArray,
27 BaseMaskedDtype,
28)
29
30if TYPE_CHECKING:
31 import pyarrow
32
33 from pandas._typing import (
34 DtypeObj,
35 npt,
36 type_t,
37 )
38
39 from pandas.core.dtypes.dtypes import ExtensionDtype
40
41
42@register_extension_dtype
43@set_module("pandas")
44class BooleanDtype(BaseMaskedDtype):
45 """
46 Extension dtype for boolean data.
47
48 This is a pandas Extension dtype for boolean data with support for
49 missing values. BooleanDtype is the dtype companion to :class:`.BooleanArray`,
50 which implements Kleene logic (sometimes called three-value logic) for
51 logical operations. See :ref:`boolean.kleene` for more.
52
53 .. warning::
54
55 BooleanDtype is considered experimental. The implementation and
56 parts of the API may change without warning.
57
58 Attributes
59 ----------
60 None
61
62 Methods
63 -------
64 None
65
66 See Also
67 --------
68 arrays.BooleanArray : Array of boolean (True/False) data with missing values.
69 Int64Dtype : Extension dtype for int64 integer data.
70 StringDtype : Extension dtype for string data.
71
72 Examples
73 --------
74 >>> pd.BooleanDtype()
75 BooleanDtype
76
77 >>> pd.array([True, False, None], dtype=pd.BooleanDtype())
78 <BooleanArray>
79 [True, False, <NA>]
80 Length: 3, dtype: boolean
81
82 >>> pd.array([True, False, None], dtype="boolean")
83 <BooleanArray>
84 [True, False, <NA>]
85 Length: 3, dtype: boolean
86 """
87
88 name: ClassVar[str] = "boolean"
89
90 # The value used to fill '_data' to avoid upcasting
91 _internal_fill_value = False
92
93 # https://github.com/python/mypy/issues/4125
94 # error: Signature of "type" incompatible with supertype "BaseMaskedDtype"
95 @property
96 def type(self) -> type: # type: ignore[override]
97 return np.bool_
98
99 @property
100 def kind(self) -> str:
101 return "b"
102
103 @property
104 def numpy_dtype(self) -> np.dtype:
105 return np.dtype("bool")
106
107 def construct_array_type(self) -> type_t[BooleanArray]:
108 """
109 Return the array type associated with this dtype.
110
111 Returns
112 -------
113 type
114 """
115 return BooleanArray
116
117 def __repr__(self) -> str:
118 return "BooleanDtype"
119
120 @property
121 def _is_boolean(self) -> bool:
122 return True
123
124 @property
125 def _is_numeric(self) -> bool:
126 return True
127
128 def __from_arrow__(
129 self, array: pyarrow.Array | pyarrow.ChunkedArray
130 ) -> BooleanArray:
131 """
132 Construct BooleanArray from pyarrow Array/ChunkedArray.
133 """
134 import pyarrow
135
136 if array.type != pyarrow.bool_() and not pyarrow.types.is_null(array.type):
137 raise TypeError(f"Expected array of boolean type, got {array.type} instead")
138
139 if isinstance(array, pyarrow.Array):
140 chunks = [array]
141 length = len(array)
142 else:
143 # pyarrow.ChunkedArray
144 chunks = array.chunks
145 length = array.length()
146
147 if pyarrow.types.is_null(array.type):
148 mask = np.ones(length, dtype=bool)
149 # No need to init data, since all null
150 data = np.empty(length, dtype=bool)
151 return BooleanArray(data, mask)
152
153 results = []
154 for arr in chunks:
155 buflist = arr.buffers()
156 data = pyarrow.BooleanArray.from_buffers(
157 arr.type, len(arr), [None, buflist[1]], offset=arr.offset
158 ).to_numpy(zero_copy_only=False)
159 if arr.null_count != 0:
160 mask = pyarrow.BooleanArray.from_buffers(
161 arr.type, len(arr), [None, buflist[0]], offset=arr.offset
162 ).to_numpy(zero_copy_only=False)
163 mask = ~mask
164 else:
165 mask = np.zeros(len(arr), dtype=bool)
166
167 bool_arr = BooleanArray(data, mask)
168 results.append(bool_arr)
169
170 if not results:
171 return BooleanArray(
172 np.array([], dtype=np.bool_), np.array([], dtype=np.bool_)
173 )
174 else:
175 return BooleanArray._concat_same_type(results)
176
177
178def coerce_to_array(
179 values, mask=None, copy: bool = False
180) -> tuple[np.ndarray, np.ndarray]:
181 """
182 Coerce the input values array to numpy arrays with a mask.
183
184 Parameters
185 ----------
186 values : 1D list-like
187 mask : bool 1D array, optional
188 copy : bool, default False
189 if True, copy the input
190
191 Returns
192 -------
193 tuple of (values, mask)
194 """
195 if isinstance(values, BooleanArray):
196 if mask is not None:
197 raise ValueError("cannot pass mask for BooleanArray input")
198 values, mask = values._data, values._mask
199 if copy:
200 values = values.copy()
201 mask = mask.copy()
202 return values, mask
203
204 mask_values = None
205 if isinstance(values, np.ndarray) and values.dtype == np.bool_:
206 if copy:
207 values = values.copy()
208 elif isinstance(values, np.ndarray) and values.dtype.kind in "iufcb":
209 mask_values = isna(values)
210
211 values_bool = np.zeros(len(values), dtype=bool)
212 values_bool[~mask_values] = values[~mask_values].astype(bool)
213
214 if not np.all(
215 values_bool[~mask_values].astype(values.dtype) == values[~mask_values]
216 ):
217 raise TypeError("Need to pass bool-like values")
218
219 values = values_bool
220 else:
221 values_object = np.asarray(values, dtype=object)
222
223 inferred_dtype = lib.infer_dtype(values_object, skipna=True)
224 integer_like = ("floating", "integer", "mixed-integer-float")
225 if inferred_dtype not in ("boolean", "empty", *integer_like):
226 raise TypeError("Need to pass bool-like values")
227
228 # mypy does not narrow the type of mask_values to npt.NDArray[np.bool_]
229 # within this branch, it assumes it can also be None
230 mask_values = cast("npt.NDArray[np.bool_]", isna(values_object))
231 values = np.zeros(len(values), dtype=bool)
232 values[~mask_values] = values_object[~mask_values].astype(bool)
233
234 # if the values were integer-like, validate it were actually 0/1's
235 if (inferred_dtype in integer_like) and not (
236 np.all(
237 values[~mask_values].astype(float)
238 == values_object[~mask_values].astype(float)
239 )
240 ):
241 raise TypeError("Need to pass bool-like values")
242
243 if mask is None and mask_values is None:
244 mask = np.zeros(values.shape, dtype=bool)
245 elif mask is None:
246 mask = mask_values
247 elif isinstance(mask, np.ndarray) and mask.dtype == np.bool_:
248 if mask_values is not None:
249 mask = mask | mask_values
250 elif copy:
251 mask = mask.copy()
252 else:
253 mask = np.array(mask, dtype=bool)
254 if mask_values is not None:
255 mask = mask | mask_values
256
257 if values.shape != mask.shape:
258 raise ValueError("values.shape and mask.shape must match")
259
260 return values, mask
261
262
263@set_module("pandas.arrays")
264class BooleanArray(BaseMaskedArray):
265 """
266 Array of boolean (True/False) data with missing values.
267
268 This is a pandas Extension array for boolean data, under the hood
269 represented by 2 numpy arrays: a boolean array with the data and
270 a boolean array with the mask (True indicating missing).
271
272 BooleanArray implements Kleene logic (sometimes called three-value
273 logic) for logical operations. See :ref:`boolean.kleene` for more.
274
275 To construct a BooleanArray from generic array-like input, use
276 :func:`pandas.array` specifying ``dtype="boolean"`` (see examples
277 below).
278
279 .. warning::
280
281 BooleanArray is considered experimental. The implementation and
282 parts of the API may change without warning.
283
284 Parameters
285 ----------
286 values : numpy.ndarray
287 A 1-d boolean-dtype array with the data.
288 mask : numpy.ndarray
289 A 1-d boolean-dtype array indicating missing values (True
290 indicates missing).
291 copy : bool, default False
292 Whether to copy the `values` and `mask` arrays.
293
294 Attributes
295 ----------
296 None
297
298 Methods
299 -------
300 None
301
302 Returns
303 -------
304 BooleanArray
305
306 See Also
307 --------
308 array : Create an array from data with the appropriate dtype.
309 BooleanDtype : Extension dtype for boolean data.
310 Series : One-dimensional ndarray with axis labels (including time series).
311 DataFrame : Two-dimensional, size-mutable, potentially heterogeneous tabular data.
312
313 Examples
314 --------
315 Create a BooleanArray with :func:`pandas.array`:
316
317 >>> pd.array([True, False, None], dtype="boolean")
318 <BooleanArray>
319 [True, False, <NA>]
320 Length: 3, dtype: boolean
321 """
322
323 _TRUE_VALUES = {"True", "TRUE", "true", "1", "1.0"}
324 _FALSE_VALUES = {"False", "FALSE", "false", "0", "0.0"}
325
326 @classmethod
327 def _simple_new(cls, values: np.ndarray, mask: npt.NDArray[np.bool_]) -> Self:
328 result = super()._simple_new(values, mask)
329 result._dtype = BooleanDtype()
330 return result
331
332 def __init__(
333 self, values: np.ndarray, mask: np.ndarray, copy: bool = False
334 ) -> None:
335 if not (isinstance(values, np.ndarray) and values.dtype == np.bool_):
336 raise TypeError(
337 "values should be boolean numpy array. Use "
338 "the 'pd.array' function instead"
339 )
340 self._dtype = BooleanDtype()
341 super().__init__(values, mask, copy=copy)
342
343 @property
344 def dtype(self) -> BooleanDtype:
345 return self._dtype
346
347 @classmethod
348 def _from_sequence_of_strings(
349 cls,
350 strings: list[str],
351 *,
352 dtype: ExtensionDtype,
353 copy: bool = False,
354 true_values: list[str] | None = None,
355 false_values: list[str] | None = None,
356 none_values: list[str] | None = None,
357 ) -> BooleanArray:
358 true_values_union = cls._TRUE_VALUES.union(true_values or [])
359 false_values_union = cls._FALSE_VALUES.union(false_values or [])
360
361 if none_values is None:
362 none_values = []
363
364 def map_string(s) -> bool | None:
365 if s in true_values_union:
366 return True
367 elif s in false_values_union:
368 return False
369 elif s in none_values:
370 return None
371 else:
372 raise ValueError(f"{s} cannot be cast to bool")
373
374 scalars = np.array(strings, dtype=object)
375 mask = isna(scalars)
376 scalars[~mask] = list(map(map_string, scalars[~mask]))
377 return cls._from_sequence(scalars, dtype=dtype, copy=copy)
378
379 _HANDLED_TYPES = (np.ndarray, numbers.Number, bool, np.bool_)
380
381 @classmethod
382 def _coerce_to_array(
383 cls, value, *, dtype: DtypeObj, copy: bool = False
384 ) -> tuple[np.ndarray, np.ndarray]:
385 if dtype:
386 assert dtype == "boolean"
387 return coerce_to_array(value, copy=copy)
388
389 def _logical_method(self, other, op):
390 assert op.__name__ in {"or_", "ror_", "and_", "rand_", "xor", "rxor"}
391 other_is_scalar = lib.is_scalar(other)
392 mask = None
393
394 if isinstance(other, BooleanArray):
395 other, mask = other._data, other._mask
396 elif is_list_like(other):
397 other = np.asarray(other, dtype="bool")
398 if other.ndim > 1:
399 return NotImplemented
400 other, mask = coerce_to_array(other, copy=False)
401 elif isinstance(other, np.bool_):
402 other = other.item()
403
404 if other_is_scalar and other is not libmissing.NA and not lib.is_bool(other):
405 raise TypeError(
406 "'other' should be pandas.NA or a bool. "
407 f"Got {type(other).__name__} instead."
408 )
409
410 if not other_is_scalar and len(self) != len(other):
411 raise ValueError("Lengths must match")
412
413 if op.__name__ in {"or_", "ror_"}:
414 result, mask = ops.kleene_or(self._data, other, self._mask, mask)
415 elif op.__name__ in {"and_", "rand_"}:
416 result, mask = ops.kleene_and(self._data, other, self._mask, mask)
417 else:
418 # i.e. xor, rxor
419 result, mask = ops.kleene_xor(self._data, other, self._mask, mask)
420
421 # i.e. BooleanArray
422 return self._maybe_mask_result(result, mask)
423
424 def _accumulate(
425 self, name: str, *, skipna: bool = True, **kwargs
426 ) -> BaseMaskedArray:
427 data = self._data
428 mask = self._mask
429 if name in ("cummin", "cummax"):
430 op = getattr(masked_accumulations, name)
431 data, mask = op(data, mask, skipna=skipna, **kwargs)
432 return self._simple_new(data, mask)
433 else:
434 from pandas.core.arrays import IntegerArray
435
436 return IntegerArray(data.astype(int), mask)._accumulate(
437 name, skipna=skipna, **kwargs
438 )