Repository: pandas-dev/pandas
Pull request #56780 (2024-01-08 18:06:29)

Backport PR #56769 on branch 2.2.x (BUG: replace matching Floats with bools for ea dtypes)
Backport PR #56769: BUG: replace matching Floats with bools for ea dtypes


Patch:
diff --git a/doc/source/whatsnew/v2.2.0.rst b/doc/source/whatsnew/v2.2.0.rst
index 2b436bc5d1..b138e91b41 100644
--- a/doc/source/whatsnew/v2.2.0.rst
+++ b/doc/source/whatsnew/v2.2.0.rst
@@ -789,6 +789,7 @@ Numeric
 - Bug in :meth:`Series.__floordiv__` and :meth:`Series.__truediv__` for :class:`ArrowDtype` with integral dtypes raising for large divisors (:issue:`56706`)
 - Bug in :meth:`Series.__floordiv__` for :class:`ArrowDtype` with integral dtypes raising for large values (:issue:`56645`)
 - Bug in :meth:`Series.pow` not filling missing values correctly (:issue:`55512`)
+- Bug in :meth:`Series.replace` and :meth:`DataFrame.replace` matching float ``0.0`` with ``False`` and vice versa (:issue:`55398`)
 
 Conversion
 ^^^^^^^^^^
diff --git a/pandas/core/missing.py b/pandas/core/missing.py
index 5dd9aaf5fb..ff45662d0b 100644
--- a/pandas/core/missing.py
+++ b/pandas/core/missing.py
@@ -31,6 +31,7 @@ from pandas.compat._optional import import_optional_dependency
 from pandas.core.dtypes.cast import infer_dtype_from
 from pandas.core.dtypes.common import (
     is_array_like,
+    is_bool_dtype,
     is_numeric_dtype,
     is_numeric_v_string_like,
     is_object_dtype,
@@ -100,21 +101,34 @@ def mask_missing(arr: ArrayLike, values_to_mask) -> npt.NDArray[np.bool_]:
 
     # GH 21977
     mask = np.zeros(arr.shape, dtype=bool)
-    for x in nonna:
-        if is_numeric_v_string_like(arr, x):
-            # GH#29553 prevent numpy deprecation warnings
-            pass
-        else:
-            if potential_na:
-                new_mask = np.zeros(arr.shape, dtype=np.bool_)
-                new_mask[arr_mask] = arr[arr_mask] == x
+    if (
+        is_numeric_dtype(arr.dtype)
+        and not is_bool_dtype(arr.dtype)
+        and is_bool_dtype(nonna.dtype)
+    ):
+        pass
+    elif (
+        is_bool_dtype(arr.dtype)
+        and is_numeric_dtype(nonna.dtype)
+        and not is_bool_dtype(nonna.dtype)
+    ):
+        pass
+    else:
+        for x in nonna:
+            if is_numeric_v_string_like(arr, x):
+                # GH#29553 prevent numpy deprecation warnings
+                pass
             else:
-                new_mask = arr == x
-
-                if not isinstance(new_mask, np.ndarray):
-                    # usually BooleanArray
-                    new_mask = new_mask.to_numpy(dtype=bool, na_value=False)
-            mask |= new_mask
+                if potential_na:
+                    new_mask = np.zeros(arr.shape, dtype=np.bool_)
+                    new_mask[arr_mask] = arr[arr_mask] == x
+                else:
+                    new_mask = arr == x
+
+                    if not isinstance(new_mask, np.ndarray):
+                        # usually BooleanArray
+                        new_mask = new_mask.to_numpy(dtype=bool, na_value=False)
+                mask |= new_mask
 
     if na_mask.any():
         mask |= isna(arr)


Test patch:
diff --git a/pandas/tests/series/methods/test_replace.py b/pandas/tests/series/methods/test_replace.py
index 4330153c18..b0f4e233ba 100644
--- a/pandas/tests/series/methods/test_replace.py
+++ b/pandas/tests/series/methods/test_replace.py
@@ -799,3 +799,15 @@ class TestSeriesReplace:
 
         ser.replace(to_replace=1, value=pd.NA, inplace=True)
         tm.assert_series_equal(ser, expected)
+
+    def test_replace_ea_float_with_bool(self):
+        # GH#55398
+        ser = pd.Series([0.0], dtype="Float64")
+        expected = ser.copy()
+        result = ser.replace(False, 1.0)
+        tm.assert_series_equal(result, expected)
+
+        ser = pd.Series([False], dtype="boolean")
+        expected = ser.copy()
+        result = ser.replace(0.0, True)
+        tm.assert_series_equal(result, expected)