Repository: pandas-dev/pandas

Issue:
x in pd.MultiIndex.drop(x)
#### Code Sample, a copy-pastable example if possible

```python
In [2]: 2 in pd.MultiIndex.from_product([[1, 2], [3, 4]]).drop(2)
Out[2]: True
```

#### Problem description

#2770 was actually more complex than what I understood when I suggested to close it. While ``remove_unused_levels()`` did solve the problem of just resetting the ``.levels``, the above is still buggy. Ironically, the above is [even tested](https://github.com/pandas-dev/pandas/blob/master/pandas/tests/frame/test_mutate_columns.py#L198).

#### Expected Output

``False``

#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: 84335621ad0a0d83302a80b6911d3985c00b5cee
python: 3.5.3.final.0
python-bits: 64
OS: Linux
OS-release: 4.9.0-4-amd64
machine: x86_64
processor: 
byteorder: little
LC_ALL: None
LANG: it_IT.UTF-8
LOCALE: it_IT.UTF-8

pandas: 0.23.0.dev0+10.g84335621a
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: 2.3.0
xlrd: 1.0.0
xlwt: 1.3.0
xlsxwriter: 0.9.6
lxml: 4.1.1
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:
this is a defined behavior. why should this not be True? The point is we don't perform this optimization as we don't really know when to do it.
It should not be ``True``... because it's not in the index: being in the ``.levels`` is an implementation detail.

And while I agree about not optimizing implicitly, then we must check both that 2 is in ``.levels[0]`` _and_ that the relative code is in the ``.labels[0]``. Or more simply: ``2 in a_multiindex`` should just be implemented as ``(2,) in a_multiindex``, which already returns the correct result.
ok I suppose this is inconsistent. ``.get_loc`` should also ``KeyError`` (correct now).
Right!

Resolving patch:
diff --git a/pandas/core/indexes/multi.py b/pandas/core/indexes/multi.py
--- a/pandas/core/indexes/multi.py
+++ b/pandas/core/indexes/multi.py
@@ -2123,6 +2123,11 @@ def _maybe_to_slice(loc):
 
         if not isinstance(key, tuple):
             loc = self._get_level_indexer(key, level=0)
+
+            # _get_level_indexer returns an empty slice if the key has
+            # been dropped from the MultiIndex
+            if isinstance(loc, slice) and loc.start == loc.stop:
+                raise KeyError(key)
             return _maybe_to_slice(loc)
 
         keylen = len(key)
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
@@ -628,7 +628,11 @@ def _convert_level_number(level_num, columns):
     levsize = len(level_labels)
     drop_cols = []
     for key in unique_groups:
-        loc = this.columns.get_loc(key)
+        try:
+            loc = this.columns.get_loc(key)
+        except KeyError:
+            drop_cols.append(key)
+            continue
 
         # can make more efficient?
         # we almost always return a slice
@@ -639,10 +643,7 @@ def _convert_level_number(level_num, columns):
         else:
             slice_len = loc.stop - loc.start
 
-        if slice_len == 0:
-            drop_cols.append(key)
-            continue
-        elif slice_len != levsize:
+        if slice_len != levsize:
             chunk = this.loc[:, this.columns[loc]]
             chunk.columns = level_vals.take(chunk.columns.labels[-1])
             value_slice = chunk.reindex(columns=level_vals_used).values


Test patch:
diff --git a/pandas/tests/frame/test_mutate_columns.py b/pandas/tests/frame/test_mutate_columns.py
--- a/pandas/tests/frame/test_mutate_columns.py
+++ b/pandas/tests/frame/test_mutate_columns.py
@@ -193,9 +193,10 @@ def test_delitem_multiindex(self):
         with pytest.raises(KeyError):
             del df[('A',)]
 
-        # xref: https://github.com/pandas-dev/pandas/issues/2770
-        # the 'A' is STILL in the columns!
-        assert 'A' in df.columns
+        # behavior of dropped/deleted MultiIndex levels changed from
+        # GH 2770 to GH 19027: MultiIndex no longer '.__contains__'
+        # levels which are dropped/deleted
+        assert 'A' not in df.columns
         with pytest.raises(KeyError):
             del df['A']
 
diff --git a/pandas/tests/indexing/test_multiindex.py b/pandas/tests/indexing/test_multiindex.py
--- a/pandas/tests/indexing/test_multiindex.py
+++ b/pandas/tests/indexing/test_multiindex.py
@@ -705,6 +705,26 @@ def test_multiindex_symmetric_difference(self):
         result = idx ^ idx2
         assert result.names == [None, None]
 
+    def test_multiindex_contains_dropped(self):
+        # GH 19027
+        # test that dropped MultiIndex levels are not in the MultiIndex
+        # despite continuing to be in the MultiIndex's levels
+        idx = MultiIndex.from_product([[1, 2], [3, 4]])
+        assert 2 in idx
+        idx = idx.drop(2)
+
+        # drop implementation keeps 2 in the levels
+        assert 2 in idx.levels[0]
+        # but it should no longer be in the index itself
+        assert 2 not in idx
+
+        # also applies to strings
+        idx = MultiIndex.from_product([['a', 'b'], ['c', 'd']])
+        assert 'a' in idx
+        idx = idx.drop('a')
+        assert 'a' in idx.levels[0]
+        assert 'a' not in idx
+
 
 class TestMultiIndexSlicers(object):