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