Skip to content

Commit 64de8f4

Browse files
authored
BUG: DataFrame.at with non-unique axes (#33047)
1 parent 8af5625 commit 64de8f4

File tree

3 files changed

+66
-7
lines changed

3 files changed

+66
-7
lines changed

doc/source/whatsnew/v1.1.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,7 @@ Indexing
467467
- Bug in :meth:`DatetimeIndex.get_loc` raising ``KeyError`` with converted-integer key instead of the user-passed key (:issue:`31425`)
468468
- Bug in :meth:`Series.xs` incorrectly returning ``Timestamp`` instead of ``datetime64`` in some object-dtype cases (:issue:`31630`)
469469
- Bug in :meth:`DataFrame.iat` incorrectly returning ``Timestamp`` instead of ``datetime`` in some object-dtype cases (:issue:`32809`)
470+
- Bug in :meth:`DataFrame.at` when either columns or index is non-unique (:issue:`33041`)
470471
- Bug in :meth:`Series.loc` and :meth:`DataFrame.loc` when indexing with an integer key on a object-dtype :class:`Index` that is not all-integers (:issue:`31905`)
471472
- Bug in :meth:`DataFrame.iloc.__setitem__` on a :class:`DataFrame` with duplicate columns incorrectly setting values for all matching columns (:issue:`15686`, :issue:`22036`)
472473
- Bug in :meth:`DataFrame.loc:` and :meth:`Series.loc` with a :class:`DatetimeIndex`, :class:`TimedeltaIndex`, or :class:`PeriodIndex` incorrectly allowing lookups of non-matching datetime-like dtypes (:issue:`32650`)

pandas/core/indexing.py

+25-7
Original file line numberDiff line numberDiff line change
@@ -2045,6 +2045,7 @@ def __setitem__(self, key, value):
20452045
key = _tuplify(self.ndim, key)
20462046
if len(key) != self.ndim:
20472047
raise ValueError("Not enough indexers for scalar access (setting)!")
2048+
20482049
key = list(self._convert_key(key, is_setter=True))
20492050
self.obj._set_value(*key, value=value, takeable=self._takeable)
20502051

@@ -2064,15 +2065,32 @@ def _convert_key(self, key, is_setter: bool = False):
20642065

20652066
return key
20662067

2068+
@property
2069+
def _axes_are_unique(self) -> bool:
2070+
# Only relevant for self.ndim == 2
2071+
assert self.ndim == 2
2072+
return self.obj.index.is_unique and self.obj.columns.is_unique
2073+
20672074
def __getitem__(self, key):
2068-
if self.ndim != 1 or not is_scalar(key):
2069-
# FIXME: is_scalar check is a kludge
2070-
return super().__getitem__(key)
20712075

2072-
# Like Index.get_value, but we do not allow positional fallback
2073-
obj = self.obj
2074-
loc = obj.index.get_loc(key)
2075-
return obj.index._get_values_for_loc(obj, loc, key)
2076+
if self.ndim == 2 and not self._axes_are_unique:
2077+
# GH#33041 fall back to .loc
2078+
if not isinstance(key, tuple) or not all(is_scalar(x) for x in key):
2079+
raise ValueError("Invalid call for scalar access (getting)!")
2080+
return self.obj.loc[key]
2081+
2082+
return super().__getitem__(key)
2083+
2084+
def __setitem__(self, key, value):
2085+
if self.ndim == 2 and not self._axes_are_unique:
2086+
# GH#33041 fall back to .loc
2087+
if not isinstance(key, tuple) or not all(is_scalar(x) for x in key):
2088+
raise ValueError("Invalid call for scalar access (setting)!")
2089+
2090+
self.obj.loc[key] = value
2091+
return
2092+
2093+
return super().__setitem__(key, value)
20762094

20772095

20782096
@doc(IndexingMixin.iat)

pandas/tests/indexing/test_scalar.py

+40
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,46 @@ def test_imethods_with_dups(self):
128128
result = df.iat[2, 0]
129129
assert result == 2
130130

131+
def test_frame_at_with_duplicate_axes(self):
132+
# GH#33041
133+
arr = np.random.randn(6).reshape(3, 2)
134+
df = DataFrame(arr, columns=["A", "A"])
135+
136+
result = df.at[0, "A"]
137+
expected = df.iloc[0]
138+
139+
tm.assert_series_equal(result, expected)
140+
141+
result = df.T.at["A", 0]
142+
tm.assert_series_equal(result, expected)
143+
144+
# setter
145+
df.at[1, "A"] = 2
146+
expected = Series([2.0, 2.0], index=["A", "A"], name=1)
147+
tm.assert_series_equal(df.iloc[1], expected)
148+
149+
def test_frame_at_with_duplicate_axes_requires_scalar_lookup(self):
150+
# GH#33041 check that falling back to loc doesn't allow non-scalar
151+
# args to slip in
152+
153+
arr = np.random.randn(6).reshape(3, 2)
154+
df = DataFrame(arr, columns=["A", "A"])
155+
156+
msg = "Invalid call for scalar access"
157+
with pytest.raises(ValueError, match=msg):
158+
df.at[[1, 2]]
159+
with pytest.raises(ValueError, match=msg):
160+
df.at[1, ["A"]]
161+
with pytest.raises(ValueError, match=msg):
162+
df.at[:, "A"]
163+
164+
with pytest.raises(ValueError, match=msg):
165+
df.at[[1, 2]] = 1
166+
with pytest.raises(ValueError, match=msg):
167+
df.at[1, ["A"]] = 1
168+
with pytest.raises(ValueError, match=msg):
169+
df.at[:, "A"] = 1
170+
131171
def test_series_at_raises_type_error(self):
132172
# at should not fallback
133173
# GH 7814

0 commit comments

Comments
 (0)