diff --git a/doc/source/whatsnew/v0.25.1.rst b/doc/source/whatsnew/v0.25.1.rst index eb60272246ebb..fa9ca98f9c8d8 100644 --- a/doc/source/whatsnew/v0.25.1.rst +++ b/doc/source/whatsnew/v0.25.1.rst @@ -128,7 +128,7 @@ Groupby/resample/rolling Reshaping ^^^^^^^^^ -- +- A ``KeyError`` is now raised if ``.unstack()`` is called on a :class:`Series` or :class:`DataFrame` with a flat :class:`Index` passing a name which is not the correct one (:issue:`18303`) - - diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index 8042f71c2754e..745f8f3c90ea8 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -1524,7 +1524,11 @@ def _validate_index_level(self, level): "Too many levels:" " Index has only 1 level, not %d" % (level + 1) ) elif level != self.name: - raise KeyError("Level %s must be same as name (%s)" % (level, self.name)) + raise KeyError( + "Requested level ({}) does not match index name ({})".format( + level, self.name + ) + ) def _get_level_number(self, level): self._validate_index_level(level) diff --git a/pandas/core/reshape/reshape.py b/pandas/core/reshape/reshape.py index 1f519d4c0867d..f5c46429d9d49 100644 --- a/pandas/core/reshape/reshape.py +++ b/pandas/core/reshape/reshape.py @@ -12,6 +12,7 @@ ensure_platform_int, is_bool_dtype, is_extension_array_dtype, + is_integer, is_integer_dtype, is_list_like, is_object_dtype, @@ -402,6 +403,10 @@ def unstack(obj, level, fill_value=None): else: level = level[0] + # Prioritize integer interpretation (GH #21677): + if not is_integer(level) and not level == "__placeholder__": + level = obj.index._get_level_number(level) + if isinstance(obj, DataFrame): if isinstance(obj.index, MultiIndex): return _unstack_frame(obj, level, fill_value=fill_value) diff --git a/pandas/tests/frame/test_alter_axes.py b/pandas/tests/frame/test_alter_axes.py index 6a274c8369328..00b59fd4dc087 100644 --- a/pandas/tests/frame/test_alter_axes.py +++ b/pandas/tests/frame/test_alter_axes.py @@ -1083,7 +1083,7 @@ def test_reset_index_level(self): # Missing levels - for both MultiIndex and single-level Index: for idx_lev in ["A", "B"], ["A"]: - with pytest.raises(KeyError, match="Level E "): + with pytest.raises(KeyError, match=r"(L|l)evel \(?E\)?"): df.set_index(idx_lev).reset_index(level=["A", "E"]) with pytest.raises(IndexError, match="Too many levels"): df.set_index(idx_lev).reset_index(level=[0, 1, 2]) diff --git a/pandas/tests/indexes/test_base.py b/pandas/tests/indexes/test_base.py index e75d80bec1fdf..c40a9bce9385b 100644 --- a/pandas/tests/indexes/test_base.py +++ b/pandas/tests/indexes/test_base.py @@ -2004,7 +2004,7 @@ def test_isin_level_kwarg_bad_label_raises(self, label, indices): msg = "'Level {} not found'" else: index = index.rename("foo") - msg = r"'Level {} must be same as name \(foo\)'" + msg = r"Requested level \({}\) does not match index name \(foo\)" with pytest.raises(KeyError, match=msg.format(label)): index.isin([], level=label) diff --git a/pandas/tests/indexes/test_common.py b/pandas/tests/indexes/test_common.py index 0e9aa07a4c05a..ae1a21e9b3980 100644 --- a/pandas/tests/indexes/test_common.py +++ b/pandas/tests/indexes/test_common.py @@ -35,7 +35,8 @@ def test_droplevel(self, indices): for level in "wrong", ["wrong"]: with pytest.raises( - KeyError, match=re.escape("'Level wrong must be same as name (None)'") + KeyError, + match=r"'Requested level \(wrong\) does not match index name \(None\)'", ): indices.droplevel(level) @@ -200,7 +201,7 @@ def test_unique(self, indices): with pytest.raises(IndexError, match=msg): indices.unique(level=3) - msg = r"Level wrong must be same as name \({}\)".format( + msg = r"Requested level \(wrong\) does not match index name \({}\)".format( re.escape(indices.name.__repr__()) ) with pytest.raises(KeyError, match=msg): diff --git a/pandas/tests/series/test_alter_axes.py b/pandas/tests/series/test_alter_axes.py index f58462c0f3576..0a25d6ba203cb 100644 --- a/pandas/tests/series/test_alter_axes.py +++ b/pandas/tests/series/test_alter_axes.py @@ -319,9 +319,9 @@ def test_reset_index_drop_errors(self): # KeyError raised for series index when passed level name is missing s = Series(range(4)) - with pytest.raises(KeyError, match="must be same as name"): + with pytest.raises(KeyError, match="does not match index name"): s.reset_index("wrong", drop=True) - with pytest.raises(KeyError, match="must be same as name"): + with pytest.raises(KeyError, match="does not match index name"): s.reset_index("wrong") # KeyError raised for series when level to be dropped is missing diff --git a/pandas/tests/test_multilevel.py b/pandas/tests/test_multilevel.py index c97c69c323b56..dc4db6e7902a8 100644 --- a/pandas/tests/test_multilevel.py +++ b/pandas/tests/test_multilevel.py @@ -524,6 +524,22 @@ def test_stack_unstack_preserve_names(self): restacked = unstacked.stack() assert restacked.index.names == self.frame.index.names + @pytest.mark.parametrize("method", ["stack", "unstack"]) + def test_stack_unstack_wrong_level_name(self, method): + # GH 18303 - wrong level name should raise + + # A DataFrame with flat axes: + df = self.frame.loc["foo"] + + with pytest.raises(KeyError, match="does not match index name"): + getattr(df, method)("mistake") + + if method == "unstack": + # Same on a Series: + s = df.iloc[:, 0] + with pytest.raises(KeyError, match="does not match index name"): + getattr(s, method)("mistake") + def test_unstack_level_name(self): result = self.frame.unstack("second") expected = self.frame.unstack(level=1)