pandas-dev/pandas · #69495

BUG: loc with bool label in object-dtype index or MultiIndex level (GH#50165)

jbrockmendel · merged Oct 11, 20263 files · 62 + / 9 −
doc/source/whatsnew/v3.2.0.rst1 + / 1 −
@@ -167,9 +167,9 @@ Interval Indexing ^^^^^^^^ - Bug in :meth:`DataFrame.loc` and :meth:`DataFrame.iloc` setitem on a 1-column :class:`DataFrame` with an extension dtype raising when the column key is an all-``False`` boolean mask or an empty list-like (:issue:`70232`)+- Bug in :meth:`DataFrame.loc` and :meth:`Series.loc` raising ``KeyError`` for a ``True`` or ``False`` label present in an object-dtype :class:`Index` or the first level of a :class:`MultiIndex` (:issue:`50165`) - Bug in :meth:`DataFrame.loc` and :meth:`Series.loc` setitem with an integer slice raising ``TypeError`` where the same ``.loc`` getitem succeeds, e.g. on an index of :class:`decimal.Decimal` values (:issue:`70219`) - Bug in :meth:`Index.difference` dropping elements that are not in ``other`` when ``other`` was a :class:`DatetimeIndex`, :class:`TimedeltaIndex` or :class:`PeriodIndex` and the elements were strings that parse to its values, or when ``other`` was an :class:`IntervalIndex` and the elements were scalars contained in its intervals (:issue:`58971`)--  Missing ^^^^^^^
pandas/core/indexing.py27 + / 8 −
@@ -18,6 +18,8 @@  from pandas._libs.indexing import NDFrameIndexerBase from pandas._libs.lib import (+    is_bool,+    is_bool_array,     is_np_dtype,     item_from_zerodim, )@@ -1738,14 +1740,7 @@ def _validate_key(self, key, axis: Axis) -> None:         # slice of integers (only if in the labels)         # boolean not in slice and with boolean index         ax = self.obj._get_axis(axis)-        if isinstance(key, bool) and not (-            is_bool_dtype(ax.dtype)-            or ax.dtype.name == "boolean"-            or (-                isinstance(ax, MultiIndex)-                and is_bool_dtype(ax.get_level_values(0).dtype)-            )-        ):+        if isinstance(key, bool) and not _holds_bool_label(ax, key):             raise KeyError(                 f"{key}: boolean label can not be used without a boolean index"             )@@ -3688,6 +3683,30 @@ def check_bool_indexer(index: Index, key) -> np.ndarray:     return check_array_indexer(index, result)  +def _holds_bool_label(index: Index, key: bool) -> bool:+    """+    Check whether a bool ``key`` can be looked up as a label in ``index``.++    For a MultiIndex, the key is matched against the first level.+    """+    if isinstance(index, MultiIndex):+        index = index.levels[0]+    if is_bool_dtype(index.dtype):+        return True+    if index.dtype != object:+        return False+    # object-dtype lookups match False to 0 and True to 1, so require that the+    # matched labels are actually bools, see GH#20432, GH#50165+    try:+        loc = index.get_loc(key)+    except KeyError:+        return False+    values = index._values[loc]+    if isinstance(values, np.ndarray):+        return is_bool_array(values)+    return is_bool(values)++ def convert_missing_indexer(indexer):     """     Reverse convert a missing indexer, which is a dict
pandas/tests/indexing/test_loc.py34 + / 0 −
@@ -3074,6 +3074,40 @@ def test_loc_bool_should_not_raise(self, frame_or_series, bool_value):         )         obj.loc[bool_value] +    @pytest.mark.parametrize(+        "index",+        [+            pd.Index([False, 42, "x"], dtype=object),+            pd.Index([False, False, "x"], dtype=object),+            pd.MultiIndex.from_tuples([(False, "a"), (42, "b"), ("x", "c")]),+        ],+    )+    def test_loc_bool_label_in_object_index(self, index):+        # GH#50165+        df = pd.DataFrame({"A": range(len(index))}, index=index)+        result = df.loc[False]+        expected = df.xs(False)+        tm.assert_equal(result, expected)+        tm.assert_equal(df["A"].loc[False], expected["A"])++    @pytest.mark.parametrize(+        "index",+        [+            pd.Index([0, 1, "x"], dtype=object),+            pd.Index([False, 0, "x"], dtype=object),+            pd.MultiIndex.from_tuples([(0, "a"), (1, "b"), ("x", "c")]),+        ],+    )+    @pytest.mark.parametrize("bool_value", [True, False])+    def test_loc_bool_label_does_not_match_int(+        self, index, frame_or_series, bool_value+    ):+        # GH#50165 object-dtype lookups would otherwise match False to 0+        obj = frame_or_series(range(3), index=index)+        msg = f"{bool_value}: boolean label can not be used without a boolean index"+        with pytest.raises(KeyError, match=msg):+            obj.loc[bool_value]+     def test_loc_bool_slice_raises(self, index, frame_or_series):         # GH20432         message = (