Skip to content

Commit 764f2df

Browse files
authored
BUG: DataFrame.at with CategoricalIndex (#41846)
1 parent 0561974 commit 764f2df

File tree

4 files changed

+35
-14
lines changed

4 files changed

+35
-14
lines changed

doc/source/whatsnew/v1.3.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,7 @@ Indexing
10171017
- Bug in :meth:`DataFrame.loc.__getitem__` with :class:`MultiIndex` casting to float when at least one index column has float dtype and we retrieve a scalar (:issue:`41369`)
10181018
- Bug in :meth:`DataFrame.loc` incorrectly matching non-Boolean index elements (:issue:`20432`)
10191019
- Bug in :meth:`Series.__delitem__` with ``ExtensionDtype`` incorrectly casting to ``ndarray`` (:issue:`40386`)
1020+
- Bug in :meth:`DataFrame.at` with a :class:`CategoricalIndex` returning incorrect results when passed integer keys (:issue:`41846`)
10201021
- Bug in :meth:`DataFrame.loc` returning a :class:`MultiIndex` in the wrong order if an indexer has duplicates (:issue:`40978`)
10211022
- Bug in :meth:`DataFrame.__setitem__` raising a ``TypeError`` when using a ``str`` subclass as the column name with a :class:`DatetimeIndex` (:issue:`37366`)
10221023
- Bug in :meth:`PeriodIndex.get_loc` failing to raise a ``KeyError`` when given a :class:`Period` with a mismatched ``freq`` (:issue:`41670`)

pandas/core/frame.py

+18-11
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@
158158
from pandas.core.indexers import check_key_length
159159
from pandas.core.indexes import base as ibase
160160
from pandas.core.indexes.api import (
161+
CategoricalIndex,
161162
DatetimeIndex,
162163
Index,
163164
PeriodIndex,
@@ -3553,6 +3554,11 @@ def _get_value(self, index, col, takeable: bool = False) -> Scalar:
35533554
Returns
35543555
-------
35553556
scalar
3557+
3558+
Notes
3559+
-----
3560+
Assumes that index and columns both have ax._index_as_unique;
3561+
caller is responsible for checking.
35563562
"""
35573563
if takeable:
35583564
series = self._ixs(col, axis=1)
@@ -3561,20 +3567,21 @@ def _get_value(self, index, col, takeable: bool = False) -> Scalar:
35613567
series = self._get_item_cache(col)
35623568
engine = self.index._engine
35633569

3570+
if isinstance(self.index, CategoricalIndex):
3571+
# Trying to use the engine fastpath may give incorrect results
3572+
# if our categories are integers that dont match our codes
3573+
col = self.columns.get_loc(col)
3574+
index = self.index.get_loc(index)
3575+
return self._get_value(index, col, takeable=True)
3576+
35643577
try:
35653578
loc = engine.get_loc(index)
35663579
return series._values[loc]
3567-
except KeyError:
3568-
# GH 20629
3569-
if self.index.nlevels > 1:
3570-
# partial indexing forbidden
3571-
raise
3572-
3573-
# we cannot handle direct indexing
3574-
# use positional
3575-
col = self.columns.get_loc(col)
3576-
index = self.index.get_loc(index)
3577-
return self._get_value(index, col, takeable=True)
3580+
except AttributeError:
3581+
# IntervalTree has no get_loc
3582+
col = self.columns.get_loc(col)
3583+
index = self.index.get_loc(index)
3584+
return self._get_value(index, col, takeable=True)
35783585

35793586
def __setitem__(self, key, value):
35803587
key = com.apply_if_callable(key, self)

pandas/core/indexing.py

+2-3
Original file line numberDiff line numberDiff line change
@@ -916,8 +916,7 @@ def __getitem__(self, key):
916916
key = tuple(list(x) if is_iterator(x) else x for x in key)
917917
key = tuple(com.apply_if_callable(x, self.obj) for x in key)
918918
if self._is_scalar_access(key):
919-
with suppress(KeyError, IndexError, AttributeError):
920-
# AttributeError for IntervalTree get_value
919+
with suppress(KeyError, IndexError):
921920
return self.obj._get_value(*key, takeable=self._takeable)
922921
return self._getitem_tuple(key)
923922
else:
@@ -1004,7 +1003,7 @@ def _is_scalar_access(self, key: tuple) -> bool:
10041003
# should not be considered scalar
10051004
return False
10061005

1007-
if not ax.is_unique:
1006+
if not ax._index_as_unique:
10081007
return False
10091008

10101009
return True

pandas/tests/indexing/test_at.py

+14
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from pandas import (
1010
CategoricalDtype,
11+
CategoricalIndex,
1112
DataFrame,
1213
Series,
1314
Timestamp,
@@ -141,3 +142,16 @@ def test_at_getitem_mixed_index_no_fallback(self):
141142
ser.at[0]
142143
with pytest.raises(KeyError, match="^4$"):
143144
ser.at[4]
145+
146+
def test_at_categorical_integers(self):
147+
# CategoricalIndex with integer categories that don't happen to match
148+
# the Categorical's codes
149+
ci = CategoricalIndex([3, 4])
150+
151+
arr = np.arange(4).reshape(2, 2)
152+
frame = DataFrame(arr, index=ci)
153+
154+
for df in [frame, frame.T]:
155+
for key in [0, 1]:
156+
with pytest.raises(KeyError, match=str(key)):
157+
df.at[key, key]

0 commit comments

Comments
 (0)