Skip to content

Commit 708c553

Browse files
committed
working
1 parent 4067e7f commit 708c553

File tree

3 files changed

+133
-48
lines changed

3 files changed

+133
-48
lines changed

doc/source/user_guide/boolean.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ Kleene Logic
2222
:class:`arrays.BooleanArray` implements Kleene logic (sometimes called three-value logic) for
2323
logical operations like ``&`` (and), ``|`` (or) and ``^`` (exclusive-or).
2424

25-
Here's a table for ``and``.
25+
This table demonstrates the results for every combination. These operations are symmetrical,
26+
so flipping the left- and right-hand side makes no difference in the result.
2627

2728
================= =========
2829
Expression Result

pandas/core/arrays/boolean.py

Lines changed: 94 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -562,13 +562,13 @@ def logical_method(self, other):
562562
# Rely on pandas to unbox and dispatch to us.
563563
return NotImplemented
564564

565+
assert op.__name__ in {"or_", "ror_", "and_", "rand_", "xor", "rxor"}
565566
other = lib.item_from_zerodim(other)
566-
omask = mask = None
567567
other_is_booleanarray = isinstance(other, BooleanArray)
568+
mask = None
568569

569570
if other_is_booleanarray:
570-
other, omask = other._data, other._mask
571-
mask = omask
571+
other, mask = other._data, other._mask
572572
elif is_list_like(other):
573573
other = np.asarray(other, dtype="bool")
574574
if other.ndim > 1:
@@ -579,41 +579,15 @@ def logical_method(self, other):
579579
raise ValueError("Lengths must match to compare")
580580
other, mask = coerce_to_array(other, copy=False)
581581

582-
# numpy will show a DeprecationWarning on invalid elementwise
583-
# comparisons, this will raise in the future
584-
if lib.is_scalar(other) and np.isnan(
585-
other
586-
): # TODO(NA): change to libmissing.NA:
587-
result = self._data
588-
mask = True
589-
else:
590-
with warnings.catch_warnings():
591-
warnings.filterwarnings("ignore", "elementwise", FutureWarning)
592-
with np.errstate(all="ignore"):
593-
result = op(self._data, other)
594-
595-
# nans propagate
596-
if mask is None:
597-
mask = self._mask
598-
else:
599-
mask = self._mask | mask
600-
601-
# Kleene-logic adjustments to the mask.
602582
if op.__name__ in {"or_", "ror_"}:
603-
mask[result] = False
583+
result, mask = kleene_or(self._data, other, self._mask, mask)
584+
return BooleanArray(result, mask)
604585
elif op.__name__ in {"and_", "rand_"}:
605-
mask[~self._data & ~self._mask] = False
606-
if other_is_booleanarray:
607-
mask[~other & ~omask] = False
608-
elif lib.is_scalar(other) and np.isnan(other): # TODO(NA): change to NA
609-
mask[:] = True
610-
# Do we ever assume that masked values are False?
611-
result[mask] = False
586+
result, mask = kleene_and(self._data, other, self._mask, mask)
587+
return BooleanArray(result, mask)
612588
elif op.__name__ in {"xor", "rxor"}:
613-
# Do we ever assume that masked values are False?
614-
result[mask] = False
615-
616-
return BooleanArray(result, mask)
589+
result, mask = kleene_xor(self._data, other, self._mask, mask)
590+
return BooleanArray(result, mask)
617591

618592
name = "__{name}__".format(name=op.__name__)
619593
return set_function_name(logical_method, name, cls)
@@ -766,6 +740,91 @@ def boolean_arithmetic_method(self, other):
766740
return set_function_name(boolean_arithmetic_method, name, cls)
767741

768742

743+
def kleene_or(left, right, left_mask, right_mask):
744+
if left_mask is None:
745+
return kleene_or(right, left, right_mask, left_mask)
746+
747+
assert left_mask is not None
748+
assert isinstance(left, np.ndarray)
749+
assert isinstance(left_mask, np.ndarray)
750+
751+
mask = left_mask
752+
753+
if right_mask is not None:
754+
mask = mask | right_mask
755+
else:
756+
mask = mask.copy()
757+
758+
# handle scalars:
759+
if lib.is_scalar(right) and np.isnan(right):
760+
result = left.copy()
761+
mask = left_mask.copy()
762+
mask[~result] = True
763+
return result, mask
764+
765+
# XXX: this implicitly relies on masked values being False!
766+
result = left | right
767+
mask[result] = False
768+
769+
# update
770+
return result, mask
771+
772+
773+
def kleene_xor(left, right, left_mask, right_mask):
774+
if left_mask is None:
775+
return kleene_xor(right, left, right_mask, left_mask)
776+
777+
result, mask = kleene_or(left, right, left_mask, right_mask)
778+
#
779+
# if lib.is_scalar(right):
780+
# if right is True:
781+
# result[result] = False
782+
# result[left & right] = False
783+
784+
if lib.is_scalar(right) and right is np.nan:
785+
mask[result] = True
786+
else:
787+
# assumes masked values are False
788+
result[left & right] = False
789+
mask[right & left_mask] = True
790+
if right_mask is not None:
791+
mask[left & right_mask] = True
792+
793+
result[mask] = False
794+
return result, mask
795+
796+
797+
def kleene_and(left, right, left_mask, right_mask):
798+
if left_mask is None:
799+
return kleene_and(right, left, right_mask, left_mask)
800+
801+
mask = left_mask
802+
803+
if right_mask is not None:
804+
mask = mask | right_mask
805+
else:
806+
mask = mask.copy()
807+
808+
if lib.is_scalar(right):
809+
result = left.copy()
810+
mask = left_mask.copy()
811+
if np.isnan(right):
812+
mask[result] = True
813+
else:
814+
result = result & right # already copied.
815+
if right is False:
816+
# unmask everything
817+
mask[:] = False
818+
else:
819+
result = left & right
820+
# unmask where either left or right is False
821+
mask[~left & ~left_mask] = False
822+
mask[~right & ~right_mask] = False
823+
824+
result[mask] = False
825+
return result, mask
826+
827+
769828
BooleanArray._add_logical_ops()
770829
BooleanArray._add_comparison_ops()
771830
BooleanArray._add_arithmetic_ops()

pandas/tests/arrays/test_boolean.py

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -413,13 +413,22 @@ def test_kleene_or(self):
413413
result = b | a
414414
tm.assert_extension_array_equal(result, expected)
415415

416-
def test_kleene_or_scalar(self):
416+
@pytest.mark.parametrize(
417+
"other, expected",
418+
[
419+
(np.nan, [True, None, None]),
420+
(True, [True, True, True]),
421+
(False, [True, False, None]),
422+
],
423+
)
424+
def test_kleene_or_scalar(self, other, expected):
425+
# TODO: test True & False
417426
a = pd.array([True, False, None], dtype="boolean")
418-
result = a | np.nan # TODO: pd.NA
419-
expected = pd.array([True, None, None], dtype="boolean")
427+
result = a | other
428+
expected = pd.array(expected, dtype="boolean")
420429
tm.assert_extension_array_equal(result, expected)
421430

422-
result = np.nan | a # TODO: pd.NA
431+
result = other | a
423432
tm.assert_extension_array_equal(result, expected)
424433

425434
@pytest.mark.parametrize(
@@ -456,13 +465,21 @@ def test_kleene_and(self):
456465
result = b & a
457466
tm.assert_extension_array_equal(result, expected)
458467

459-
def test_kleene_and_scalar(self):
468+
@pytest.mark.parametrize(
469+
"other, expected",
470+
[
471+
(np.nan, [None, False, None]),
472+
(True, [True, False, None]),
473+
(False, [False, False, False]),
474+
],
475+
)
476+
def test_kleene_and_scalar(self, other, expected):
460477
a = pd.array([True, False, None], dtype="boolean")
461-
result = a & np.nan # TODO: pd.NA
462-
expected = pd.array([None, None, None], dtype="boolean")
478+
result = a & other
479+
expected = pd.array(expected, dtype="boolean")
463480
tm.assert_extension_array_equal(result, expected)
464481

465-
result = np.nan & a # TODO: pd.na
482+
result = other & a
466483
tm.assert_extension_array_equal(result, expected)
467484

468485
def test_kleene_xor(self):
@@ -477,13 +494,21 @@ def test_kleene_xor(self):
477494
result = b ^ a
478495
tm.assert_extension_array_equal(result, expected)
479496

480-
def test_kleene_scalar(self):
497+
@pytest.mark.parametrize(
498+
"other, expected",
499+
[
500+
(np.nan, [None, None, None]),
501+
(True, [False, True, None]),
502+
(False, [True, False, None]),
503+
],
504+
)
505+
def test_kleene_xor_scalar(self, other, expected):
481506
a = pd.array([True, False, None], dtype="boolean")
482-
result = a ^ np.nan # TODO: pd.NA
483-
expected = pd.array([None, None, None], dtype="boolean")
507+
result = a ^ other
508+
expected = pd.array(expected, dtype="boolean")
484509
tm.assert_extension_array_equal(result, expected)
485510

486-
result = np.nan ^ a # TODO: pd.NA
511+
result = other ^ a
487512
tm.assert_extension_array_equal(result, expected)
488513

489514

0 commit comments

Comments
 (0)