123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189 |
- """
- Ops for masked arrays.
- """
- from __future__ import annotations
- import numpy as np
- from pandas._libs import (
- lib,
- missing as libmissing,
- )
- def kleene_or(
- left: bool | np.ndarray | libmissing.NAType,
- right: bool | np.ndarray | libmissing.NAType,
- left_mask: np.ndarray | None,
- right_mask: np.ndarray | None,
- ):
- """
- Boolean ``or`` using Kleene logic.
- Values are NA where we have ``NA | NA`` or ``NA | False``.
- ``NA | True`` is considered True.
- Parameters
- ----------
- left, right : ndarray, NA, or bool
- The values of the array.
- left_mask, right_mask : ndarray, optional
- The masks. Only one of these may be None, which implies that
- the associated `left` or `right` value is a scalar.
- Returns
- -------
- result, mask: ndarray[bool]
- The result of the logical or, and the new mask.
- """
- # To reduce the number of cases, we ensure that `left` & `left_mask`
- # always come from an array, not a scalar. This is safe, since
- # A | B == B | A
- if left_mask is None:
- return kleene_or(right, left, right_mask, left_mask)
- if not isinstance(left, np.ndarray):
- raise TypeError("Either `left` or `right` need to be a np.ndarray.")
- raise_for_nan(right, method="or")
- if right is libmissing.NA:
- result = left.copy()
- else:
- result = left | right
- if right_mask is not None:
- # output is unknown where (False & NA), (NA & False), (NA & NA)
- left_false = ~(left | left_mask)
- right_false = ~(right | right_mask)
- mask = (
- (left_false & right_mask)
- | (right_false & left_mask)
- | (left_mask & right_mask)
- )
- else:
- if right is True:
- mask = np.zeros_like(left_mask)
- elif right is libmissing.NA:
- mask = (~left & ~left_mask) | left_mask
- else:
- # False
- mask = left_mask.copy()
- return result, mask
- def kleene_xor(
- left: bool | np.ndarray | libmissing.NAType,
- right: bool | np.ndarray | libmissing.NAType,
- left_mask: np.ndarray | None,
- right_mask: np.ndarray | None,
- ):
- """
- Boolean ``xor`` using Kleene logic.
- This is the same as ``or``, with the following adjustments
- * True, True -> False
- * True, NA -> NA
- Parameters
- ----------
- left, right : ndarray, NA, or bool
- The values of the array.
- left_mask, right_mask : ndarray, optional
- The masks. Only one of these may be None, which implies that
- the associated `left` or `right` value is a scalar.
- Returns
- -------
- result, mask: ndarray[bool]
- The result of the logical xor, and the new mask.
- """
- # To reduce the number of cases, we ensure that `left` & `left_mask`
- # always come from an array, not a scalar. This is safe, since
- # A ^ B == B ^ A
- if left_mask is None:
- return kleene_xor(right, left, right_mask, left_mask)
- if not isinstance(left, np.ndarray):
- raise TypeError("Either `left` or `right` need to be a np.ndarray.")
- raise_for_nan(right, method="xor")
- if right is libmissing.NA:
- result = np.zeros_like(left)
- else:
- result = left ^ right
- if right_mask is None:
- if right is libmissing.NA:
- mask = np.ones_like(left_mask)
- else:
- mask = left_mask.copy()
- else:
- mask = left_mask | right_mask
- return result, mask
- def kleene_and(
- left: bool | libmissing.NAType | np.ndarray,
- right: bool | libmissing.NAType | np.ndarray,
- left_mask: np.ndarray | None,
- right_mask: np.ndarray | None,
- ):
- """
- Boolean ``and`` using Kleene logic.
- Values are ``NA`` for ``NA & NA`` or ``True & NA``.
- Parameters
- ----------
- left, right : ndarray, NA, or bool
- The values of the array.
- left_mask, right_mask : ndarray, optional
- The masks. Only one of these may be None, which implies that
- the associated `left` or `right` value is a scalar.
- Returns
- -------
- result, mask: ndarray[bool]
- The result of the logical xor, and the new mask.
- """
- # To reduce the number of cases, we ensure that `left` & `left_mask`
- # always come from an array, not a scalar. This is safe, since
- # A & B == B & A
- if left_mask is None:
- return kleene_and(right, left, right_mask, left_mask)
- if not isinstance(left, np.ndarray):
- raise TypeError("Either `left` or `right` need to be a np.ndarray.")
- raise_for_nan(right, method="and")
- if right is libmissing.NA:
- result = np.zeros_like(left)
- else:
- result = left & right
- if right_mask is None:
- # Scalar `right`
- if right is libmissing.NA:
- mask = (left & ~left_mask) | left_mask
- else:
- mask = left_mask.copy()
- if right is False:
- # unmask everything
- mask[:] = False
- else:
- # unmask where either left or right is False
- left_false = ~(left | left_mask)
- right_false = ~(right | right_mask)
- mask = (left_mask & ~right_false) | (right_mask & ~left_false)
- return result, mask
- def raise_for_nan(value, method: str) -> None:
- if lib.is_float(value) and np.isnan(value):
- raise ValueError(f"Cannot perform logical '{method}' with floating NaN")
|