diff --git a/pandas/core/ops/array_ops.py b/pandas/core/ops/array_ops.py index 7b21772b443f6..f383b2ce77195 100644 --- a/pandas/core/ops/array_ops.py +++ b/pandas/core/ops/array_ops.py @@ -113,6 +113,10 @@ def fill_binop(left, right, fill_value): def comp_method_OBJECT_ARRAY(op, x, y): + from pandas._libs import missing as libmissing + + from pandas.core.arrays import BooleanArray + if isinstance(y, list): # e.g. test_tuple_categories y = construct_1d_object_array_from_listlike(y) @@ -129,7 +133,28 @@ def comp_method_OBJECT_ARRAY(op, x, y): result = libops.vec_compare(x.ravel(), y.ravel(), op) else: result = libops.scalar_compare(x.ravel(), y, op) - return result.reshape(x.shape) + result = result.reshape(x.shape) + + # GH#63328: Check if there are pd.NA values in the input and return + # BooleanArray to properly propagate NA in comparisons + x_has_na = any(val is libmissing.NA for val in x.ravel()) + y_has_na = (is_scalar(y) and y is libmissing.NA) or ( + isinstance(y, np.ndarray) and any(val is libmissing.NA for val in y.ravel()) + ) + + if x_has_na or y_has_na: + # Create a mask for NA values + mask = np.array([val is libmissing.NA for val in x.ravel()], dtype=bool) + if isinstance(y, np.ndarray): + mask = mask | np.array( + [val is libmissing.NA for val in y.ravel()], dtype=bool + ) + elif y is libmissing.NA: + mask = np.ones(x.shape, dtype=bool) + mask = mask.reshape(x.shape) + return BooleanArray(result, mask, copy=False) + + return result def _masked_arith_op(x: np.ndarray, y, op) -> np.ndarray: diff --git a/pandas/tests/series/test_arithmetic.py b/pandas/tests/series/test_arithmetic.py index a77e55612e23d..7c0598b0cff1e 100644 --- a/pandas/tests/series/test_arithmetic.py +++ b/pandas/tests/series/test_arithmetic.py @@ -803,6 +803,20 @@ def test_compare_series_interval_keyword(self): expected = Series([True, False, False]) tm.assert_series_equal(result, expected) + @pytest.mark.parametrize("comparison_op", [operator.eq, operator.ne]) + def test_comparison_with_na_object_dtype(self, comparison_op): + # GH#63328 - NA comparison should propagate NA in results + ser = Series([1, 2, pd.NA]) + + result = comparison_op(ser, 3) + + if comparison_op is operator.eq: + expected = Series([False, False, pd.NA], dtype="boolean") + else: + expected = Series([True, True, pd.NA], dtype="boolean") + + tm.assert_series_equal(result, expected) + # ------------------------------------------------------------------ # Unsorted