Repository: pandas-dev/pandas

Issue:
DataFrame.unstack(label) accepts wrong labels if index is flat
#### Code Sample, a copy-pastable example if possible

```python
In [2]: df = pd.DataFrame(index=range(2), columns=pd.MultiIndex.from_product([range(2), range(2)]))

In [3]: df.unstack('mistake')
Out[3]: 
0  0  0    NaN
      1    NaN
   1  0    NaN
      1    NaN
1  0  0    NaN
      1    NaN
   1  0    NaN
      1    NaN
dtype: object


```
#### Problem description

An error should be raised.

#### Expected Output

``KeyError``

#### Output of ``pd.show_versions()``

<details>

In [4]: pd.show_versions()

INSTALLED VERSIONS
------------------
commit: 63e8527d32aaf6afe1cd4b2a7b3bfadb088c9a72
python: 3.5.3.final.0
python-bits: 64
OS: Linux
OS-release: 4.9.0-3-amd64
machine: x86_64
processor: 
byteorder: little
LC_ALL: None
LANG: it_IT.UTF-8
LOCALE: it_IT.UTF-8

pandas: 0.22.0.dev0+131.g63e8527d3
pytest: 3.2.3
pip: 9.0.1
setuptools: 36.7.0
Cython: 0.25.2
numpy: 1.12.1
scipy: 0.19.0
pyarrow: None
xarray: None
IPython: 6.2.1
sphinx: 1.5.6
patsy: 0.4.1
dateutil: 2.6.1
pytz: 2017.2
blosc: None
bottleneck: 1.2.0dev
tables: 3.3.0
numexpr: 2.6.1
feather: 0.3.1
matplotlib: 2.0.0
openpyxl: None
xlrd: 1.0.0
xlwt: 1.1.2
xlsxwriter: 0.9.6
lxml: None
bs4: 4.5.3
html5lib: 0.999999999
sqlalchemy: 1.0.15
pymysql: None
psycopg2: None
jinja2: 2.10
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: 0.2.1


</details>



Discussion:
@toobaz : Do you know if there was a release where an error should have been raised, or are you suggesting that we change the API to raise `KeyError` in a situation like this?
Yes, I'm suggesting it should raise a ``KeyError``, like ``pd.Index([]).get_level_values('mistake')`` does. 

By the way, I've always found the error message ``KeyError: 'Level mistake must be same as name (None)'`` raised by the latter confusing... I would suggest harmonizing with the error raised by ``MultiIndex.get_level_values("mistake")`` (that is, ``KeyError: 'Level "mistake" not found'``), and/or to add info on the available names (``KeyError: 'Level "mistake" not found in index level names ([None])'``).
@toobaz : Ah, okay, so not quite answering my question, BUT that's fine since your answer is pretty good justification for the issue anyhow.

As for your other point, yes, that's also fair.  If you want, you can open another issue for that OR just open a PR to patch the behavior if it's clear how to fix.

Resolving patch:
diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py
--- 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
--- 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)


Test patch:
diff --git a/pandas/tests/frame/test_alter_axes.py b/pandas/tests/frame/test_alter_axes.py
--- 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
--- 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
--- 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
--- 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
--- 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)