pandas-dev/pandas · #69495
BUG: loc with bool label in object-dtype index or MultiIndex level (GH#50165)
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 dictpandas/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 = (