From 083b1ecd7034ad92007018f8f72925ac8a831bc4 Mon Sep 17 00:00:00 2001 From: rhshadrach Date: Sat, 7 Mar 2020 19:35:20 +0100 Subject: [PATCH 1/4] [BUG] fix .at for multiindexed series Addresses: GH26989 --- doc/source/whatsnew/v1.1.0.rst | 2 +- pandas/core/indexing.py | 9 ++++++-- pandas/tests/indexing/test_scalar.py | 33 ++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index a723983590650..4affee53e64e8 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -717,7 +717,7 @@ Indexing - Bug in :meth:`Series.__setitem__` with an :class:`IntervalIndex` and a list-like key of integers (:issue:`33473`) - Bug in :meth:`Series.__getitem__` allowing missing labels with ``np.ndarray``, :class:`Index`, :class:`Series` indexers but not ``list``, these now all raise ``KeyError`` (:issue:`33646`) - Bug in :meth:`DataFrame.truncate` and :meth:`Series.truncate` where index was assumed to be monotone increasing (:issue:`33756`) -- Indexing with a list of strings representing datetimes failed on :class:`DatetimeIndex` or :class:`PeriodIndex`(:issue:`11278`) +- Bug in :meth:`Series.at` when used with a :class:`MultiIndex` would raise an exception on valid inputs (:issue:`26989`) Missing ^^^^^^^ diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index b857a59195695..0552376353dab 100644 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -2016,10 +2016,10 @@ def __setitem__(self, key, value): if not isinstance(key, tuple): key = _tuplify(self.ndim, key) + key = list(self._convert_key(key, is_setter=True)) if len(key) != self.ndim: raise ValueError("Not enough indexers for scalar access (setting)!") - - key = list(self._convert_key(key, is_setter=True)) + self.obj._set_value(*key, value=value, takeable=self._takeable) @@ -2032,6 +2032,11 @@ def _convert_key(self, key, is_setter: bool = False): Require they keys to be the same type as the index. (so we don't fallback) """ + # For series, unpacking key needs to result in the label. + # This is already the case for len(key) == 1; e.g. (1,) + if isinstance(self.obj, ABCSeries) and len(key) > 1: + key = (key,) + # allow arbitrary setting if is_setter: return list(key) diff --git a/pandas/tests/indexing/test_scalar.py b/pandas/tests/indexing/test_scalar.py index 216d554e22b49..e153899e50795 100644 --- a/pandas/tests/indexing/test_scalar.py +++ b/pandas/tests/indexing/test_scalar.py @@ -351,3 +351,36 @@ def test_iat_series_with_period_index(): expected = ser[index[0]] result = ser.iat[0] assert expected == result + + +def test_tuple_indexed_series_at_get(): + # GH 26989 + # Series.at works with MultiIndex + series = Series([1, 2], index=[(1, 2), (3, 4)]) + assert series.at[1, 2] == 1 + + +def test_tuple_indexed_series_at_set(): + # GH 26989 + # Series.at works with MultiIndex + series = Series([1, 2], index=[(1, 2), (3, 4)]) + series.at[1, 2] = 3 + assert series.at[1, 2] == 3 + + +def test_multiindex_series_at_get(): + # GH 26989 + # Series.at works with MultiIndex + series = Series([1, 2], index=[[1, 2], [3, 4]]) + assert series.at[1, 3] == 1 + assert series.loc[1, 3] == 1 + + +def test_multiindex_series_at_set(): + # GH 26989 + # Series.at works with MultiIndex + series = Series([1, 2], index=[[1, 2], [3, 4]]) + series.at[1, 3] = 3 + assert series.at[1, 3] == 3 + series.loc[1, 3] = 4 + assert series.loc[1, 3] == 4 From 88bca0410e57175c8452d3eebb9eb307846ae571 Mon Sep 17 00:00:00 2001 From: Richard Date: Fri, 27 Mar 2020 15:04:34 -0400 Subject: [PATCH 2/4] Replaced isinstance check, fixed comments, added asserts in tests --- pandas/core/indexing.py | 3 ++- pandas/tests/indexing/test_scalar.py | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index 0552376353dab..d61f4452f6521 100644 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -2032,9 +2032,10 @@ def _convert_key(self, key, is_setter: bool = False): Require they keys to be the same type as the index. (so we don't fallback) """ + # GH 26989 # For series, unpacking key needs to result in the label. # This is already the case for len(key) == 1; e.g. (1,) - if isinstance(self.obj, ABCSeries) and len(key) > 1: + if self.ndim == 1 and len(key) > 1: key = (key,) # allow arbitrary setting diff --git a/pandas/tests/indexing/test_scalar.py b/pandas/tests/indexing/test_scalar.py index e153899e50795..8e7f5b0ae1794 100644 --- a/pandas/tests/indexing/test_scalar.py +++ b/pandas/tests/indexing/test_scalar.py @@ -355,16 +355,18 @@ def test_iat_series_with_period_index(): def test_tuple_indexed_series_at_get(): # GH 26989 - # Series.at works with MultiIndex + # Series.at works with index of tuples series = Series([1, 2], index=[(1, 2), (3, 4)]) + assert series.index.nlevels == 1 assert series.at[1, 2] == 1 def test_tuple_indexed_series_at_set(): # GH 26989 - # Series.at works with MultiIndex + # Series.at works with index of tuples series = Series([1, 2], index=[(1, 2), (3, 4)]) series.at[1, 2] = 3 + assert series.index.nlevels == 1 assert series.at[1, 2] == 3 @@ -372,6 +374,7 @@ def test_multiindex_series_at_get(): # GH 26989 # Series.at works with MultiIndex series = Series([1, 2], index=[[1, 2], [3, 4]]) + assert series.index.nlevels == 2 assert series.at[1, 3] == 1 assert series.loc[1, 3] == 1 @@ -380,6 +383,7 @@ def test_multiindex_series_at_set(): # GH 26989 # Series.at works with MultiIndex series = Series([1, 2], index=[[1, 2], [3, 4]]) + assert series.index.nlevels == 2 series.at[1, 3] = 3 assert series.at[1, 3] == 3 series.loc[1, 3] = 4 From f325e09f5b304ad72280c4a33623dfcc319ef186 Mon Sep 17 00:00:00 2001 From: Richard Date: Tue, 31 Mar 2020 05:59:46 -0400 Subject: [PATCH 3/4] Added tests for DataFrame alongside Series --- pandas/core/indexing.py | 2 +- pandas/tests/indexing/test_scalar.py | 61 ++++++++++++++++++++-------- 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/pandas/core/indexing.py b/pandas/core/indexing.py index d61f4452f6521..3a146bb0438c5 100644 --- a/pandas/core/indexing.py +++ b/pandas/core/indexing.py @@ -2019,7 +2019,7 @@ def __setitem__(self, key, value): key = list(self._convert_key(key, is_setter=True)) if len(key) != self.ndim: raise ValueError("Not enough indexers for scalar access (setting)!") - + self.obj._set_value(*key, value=value, takeable=self._takeable) diff --git a/pandas/tests/indexing/test_scalar.py b/pandas/tests/indexing/test_scalar.py index 8e7f5b0ae1794..4337f01ea33e0 100644 --- a/pandas/tests/indexing/test_scalar.py +++ b/pandas/tests/indexing/test_scalar.py @@ -353,38 +353,63 @@ def test_iat_series_with_period_index(): assert expected == result -def test_tuple_indexed_series_at_get(): +def test_at_with_tuple_index_get(): # GH 26989 - # Series.at works with index of tuples - series = Series([1, 2], index=[(1, 2), (3, 4)]) + # DataFrame.at getter works with Index of tuples + df = DataFrame({"a": [1, 2]}, index=[(1, 2), (3, 4)]) + assert df.index.nlevels == 1 + assert df.at[(1, 2), "a"] == 1 + + # Series.at getter works with Index of tuples + series = df["a"] assert series.index.nlevels == 1 - assert series.at[1, 2] == 1 + assert series.at[(1, 2)] == 1 -def test_tuple_indexed_series_at_set(): +def test_at_with_tuple_index_set(): # GH 26989 - # Series.at works with index of tuples - series = Series([1, 2], index=[(1, 2), (3, 4)]) - series.at[1, 2] = 3 + # DataFrame.at setter works with Index of tuples + df = DataFrame({"a": [1, 2]}, index=[(1, 2), (3, 4)]) + assert df.index.nlevels == 1 + df.at[(1, 2), "a"] = 2 + assert df.at[(1, 2), "a"] == 2 + + # Series.at setter works with Index of tuples + series = df["a"] assert series.index.nlevels == 1 + series.at[1, 2] = 3 assert series.at[1, 2] == 3 -def test_multiindex_series_at_get(): +def test_multiindex_at_get(): # GH 26989 - # Series.at works with MultiIndex - series = Series([1, 2], index=[[1, 2], [3, 4]]) + # DataFrame.at and DataFrame.loc getter works with MultiIndex + df = DataFrame({"a": [1, 2]}, index=[[1, 2], [3, 4]]) + assert df.index.nlevels == 2 + assert df.at[(1, 3), "a"] == 1 + assert df.loc[(1, 3), "a"] == 1 + + # Series.at and Series.loc getter works with MultiIndex + series = df["a"] assert series.index.nlevels == 2 assert series.at[1, 3] == 1 assert series.loc[1, 3] == 1 -def test_multiindex_series_at_set(): +def test_multiindex_at_set(): # GH 26989 - # Series.at works with MultiIndex - series = Series([1, 2], index=[[1, 2], [3, 4]]) + # DataFrame.at and DataFrame.loc setter works with MultiIndex + df = DataFrame({"a": [1, 2]}, index=[[1, 2], [3, 4]]) + assert df.index.nlevels == 2 + df.at[(1, 3), "a"] = 3 + assert df.at[(1, 3), "a"] == 3 + df.loc[(1, 3), "a"] = 4 + assert df.loc[(1, 3), "a"] == 4 + + # Series.at and Series.loc setter works with MultiIndex + series = df["a"] assert series.index.nlevels == 2 - series.at[1, 3] = 3 - assert series.at[1, 3] == 3 - series.loc[1, 3] = 4 - assert series.loc[1, 3] == 4 + series.at[1, 3] = 5 + assert series.at[1, 3] == 5 + series.loc[1, 3] = 6 + assert series.loc[1, 3] == 6 From d03b102c6ecb1a4eca2b63acbdaf27f571e5f0fd Mon Sep 17 00:00:00 2001 From: Richard Date: Sun, 17 May 2020 18:12:14 -0400 Subject: [PATCH 4/4] Reverted whatsnew line removal --- doc/source/whatsnew/v1.1.0.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v1.1.0.rst b/doc/source/whatsnew/v1.1.0.rst index 4affee53e64e8..d77e52e00e22d 100644 --- a/doc/source/whatsnew/v1.1.0.rst +++ b/doc/source/whatsnew/v1.1.0.rst @@ -717,6 +717,7 @@ Indexing - Bug in :meth:`Series.__setitem__` with an :class:`IntervalIndex` and a list-like key of integers (:issue:`33473`) - Bug in :meth:`Series.__getitem__` allowing missing labels with ``np.ndarray``, :class:`Index`, :class:`Series` indexers but not ``list``, these now all raise ``KeyError`` (:issue:`33646`) - Bug in :meth:`DataFrame.truncate` and :meth:`Series.truncate` where index was assumed to be monotone increasing (:issue:`33756`) +- Indexing with a list of strings representing datetimes failed on :class:`DatetimeIndex` or :class:`PeriodIndex`(:issue:`11278`) - Bug in :meth:`Series.at` when used with a :class:`MultiIndex` would raise an exception on valid inputs (:issue:`26989`) Missing