diff --git a/doc/source/user_guide/indexing.rst b/doc/source/user_guide/indexing.rst index 92729a16c6a30..03843c08cf94f 100644 --- a/doc/source/user_guide/indexing.rst +++ b/doc/source/user_guide/indexing.rst @@ -851,16 +851,12 @@ You can also set using these same indexers. .. ipython:: python - df.at[dates[5], 'E'] = 7 + df.at[dates[5], 'D'] = 7 df.iat[3, 0] = 7 - -``at`` may enlarge the object in-place as above if the indexer is missing. - -.. ipython:: python - - df.at[dates[-1] + pd.Timedelta('1 day'), 0] = 7 df +``at`` will not enlarge the object in-place if the indexer is missing. + Boolean indexing ---------------- diff --git a/doc/source/whatsnew/v1.6.0.rst b/doc/source/whatsnew/v1.6.0.rst index ee5085fd9ad89..feda2b564bff8 100644 --- a/doc/source/whatsnew/v1.6.0.rst +++ b/doc/source/whatsnew/v1.6.0.rst @@ -39,10 +39,41 @@ Notable bug fixes These are bug fixes that might have notable behavior changes. -.. _whatsnew_160.notable_bug_fixes.notable_bug_fix1: +.. _whatsnew_160.notable_bug_fixes.at_DataFrame_expand: -notable_bug_fix1 -^^^^^^^^^^^^^^^^ +Using DataFrame.at to expand DataFrame +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:meth:`DataFrame.at` would allow the addition of columns and rows to a DataFrame. (:issue:`48323`) + +.. code-block:: ipython + + In [3]: frame = pd.DataFrame({"a": [1, 2]}) + In [4]: frame + Out[4]: + a + 0 1 + 1 2 + +*Old Behavior* + +.. code-block:: ipython + + In [5]: frame.at[2, "a"] = 7 + In [6]: frame + Out[6]: + a + 0 1 + 1 2 + 2 7 + +*New Behavior* + +.. code-block:: ipython + + In [5]: frame.at[2, "a"] = 7 + Out[5]: + KeyError: 2 .. _whatsnew_160.notable_bug_fixes.notable_bug_fix2: diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index 27ca114519f77..97804afb34589 100644 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -551,8 +551,8 @@ def at(self) -> _AtIndexer: Raises ------ KeyError - * If getting a value and 'label' does not exist in a DataFrame or - Series. + * If getting or setting a value and 'label' does not exist in a + DataFrame or Series. ValueError * If row/column label pair is not a tuple or if any label from the pair is not a scalar for DataFrame. @@ -560,7 +560,6 @@ def at(self) -> _AtIndexer: See Also -------- - DataFrame.at : Access a single value for a row/column pair by label. DataFrame.iat : Access a single value for a row/column pair by integer position. DataFrame.loc : Access a group of rows and columns by label(s). @@ -2429,6 +2428,9 @@ def __getitem__(self, key): return super().__getitem__(key) def __setitem__(self, key, value): + # raises exception if key does not exist + self.__getitem__(key) + if self.ndim == 2 and not self._axes_are_unique: # GH#33041 fall back to .loc if not isinstance(key, tuple) or not all(is_scalar(x) for x in key): diff --git a/pandas/tests/indexes/multi/test_get_set.py b/pandas/tests/indexes/multi/test_get_set.py index 42cf0168f6599..046ec4014f2c8 100644 --- a/pandas/tests/indexes/multi/test_get_set.py +++ b/pandas/tests/indexes/multi/test_get_set.py @@ -415,7 +415,7 @@ def test_set_value_keeps_names(): df = df.sort_index() assert df._is_copy is None assert df.index.names == ("Name", "Number") - df.at[("grethe", "4"), "one"] = 99.34 + df.loc[("grethe", "4"), "one"] = 99.34 assert df._is_copy is None assert df.index.names == ("Name", "Number") diff --git a/pandas/tests/indexing/test_at.py b/pandas/tests/indexing/test_at.py index f6d2fb12c5d81..3e92e40897579 100644 --- a/pandas/tests/indexing/test_at.py +++ b/pandas/tests/indexing/test_at.py @@ -6,8 +6,6 @@ import numpy as np import pytest -from pandas.errors import InvalidIndexError - from pandas import ( CategoricalDtype, CategoricalIndex, @@ -105,12 +103,15 @@ def test_at_setitem_multiindex(self): np.zeros((3, 2), dtype="int64"), columns=MultiIndex.from_tuples([("a", 0), ("a", 1)]), ) - df.at[0, "a"] = 10 + df.at[0, ("a", 0)] = 10 + df.at[0, ("a", 1)] = 10 expected = DataFrame( [[10, 10], [0, 0], [0, 0]], columns=MultiIndex.from_tuples([("a", 0), ("a", 1)]), ) tm.assert_frame_equal(df, expected) + with pytest.raises(TypeError, match=""): + df.at[0, "a"] = 11 @pytest.mark.parametrize("row", (Timestamp("2019-01-01"), "2019-01-01")) def test_at_datetime_index(self, row): @@ -126,11 +127,13 @@ def test_at_datetime_index(self, row): tm.assert_frame_equal(df, expected) -class TestAtSetItemWithExpansion: - def test_at_setitem_expansion_series_dt64tz_value(self, tz_naive_fixture): +class TestAtSetTzItem: + def test_at_setitem_series_dt64tz_value(self, tz_naive_fixture): # GH#25506 + # Modified in GH#48323 due to .at change ts = Timestamp("2017-08-05 00:00:00+0100", tz=tz_naive_fixture) - result = Series(ts) + ts2 = Timestamp("2017-09-05 00:00:00+0100", tz=tz_naive_fixture) + result = Series([ts, ts2]) result.at[1] = ts expected = Series([ts, ts]) tm.assert_series_equal(result, expected) @@ -211,7 +214,7 @@ def test_at_frame_raises_key_error2(self, indexer_al): def test_at_frame_multiple_columns(self): # GH#48296 - at shouldn't modify multiple columns df = DataFrame({"a": [1, 2], "b": [3, 4]}) - with pytest.raises(InvalidIndexError, match=r"slice\(None, None, None\)"): + with pytest.raises(TypeError, match="col"): df.at[5] = [6, 7] def test_at_getitem_mixed_index_no_fallback(self): @@ -234,3 +237,9 @@ def test_at_categorical_integers(self): for key in [0, 1]: with pytest.raises(KeyError, match=str(key)): df.at[key, key] + + def test_at_does_not_expand(self): + # GH#48323 + frame = DataFrame({"a": [1, 2]}) + with pytest.raises(KeyError, match="b"): + frame.at[2, "b"] = 9 diff --git a/pandas/tests/indexing/test_partial.py b/pandas/tests/indexing/test_partial.py index 938056902e745..2d3e1be495ef5 100644 --- a/pandas/tests/indexing/test_partial.py +++ b/pandas/tests/indexing/test_partial.py @@ -343,9 +343,6 @@ def test_partial_setting2(self): df = df_orig.copy() df.loc[dates[-1] + dates.freq, "A"] = 7 tm.assert_frame_equal(df, expected) - df = df_orig.copy() - df.at[dates[-1] + dates.freq, "A"] = 7 - tm.assert_frame_equal(df, expected) exp_other = DataFrame({0: 7}, index=dates[-1:] + dates.freq) expected = pd.concat([df_orig, exp_other], axis=1) @@ -353,9 +350,6 @@ def test_partial_setting2(self): df = df_orig.copy() df.loc[dates[-1] + dates.freq, 0] = 7 tm.assert_frame_equal(df, expected) - df = df_orig.copy() - df.at[dates[-1] + dates.freq, 0] = 7 - tm.assert_frame_equal(df, expected) def test_partial_setting_mixed_dtype(self):