Repository: pandas-dev/pandas

Issue:
Passing empty label list to df.drop() errors when index is non-unique
#### Code Sample, a copy-pastable example if possible

```python
# Unique index works fine
In [8]: pd.DataFrame(index=[1,2,3]).drop([])
Out[8]: 
Empty DataFrame
Columns: []
Index: [1, 2, 3]

# Non-unique index throws error
In [9]: pd.DataFrame(index=[1,1,2]).drop([])
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-9-e714e59114d2> in <module>()
----> 1 pd.DataFrame(index=[1,1,2]).drop([])

~/.virtualenvs/b5/lib/python3.6/site-packages/pandas/core/frame.py in drop(self, labels, axis, index, columns, level, inplace, errors)
   3692                                            index=index, columns=columns,
   3693                                            level=level, inplace=inplace,
-> 3694                                            errors=errors)
   3695 
   3696     @rewrite_axis_style_signature('mapper', [('copy', True),

~/.virtualenvs/b5/lib/python3.6/site-packages/pandas/core/generic.py in drop(self, labels, axis, index, columns, level, inplace, errors)
   3106         for axis, labels in axes.items():
   3107             if labels is not None:
-> 3108                 obj = obj._drop_axis(labels, axis, level=level, errors=errors)
   3109 
   3110         if inplace:

~/.virtualenvs/b5/lib/python3.6/site-packages/pandas/core/generic.py in _drop_axis(self, labels, axis, level, errors)
   3156 
   3157             if errors == 'raise' and indexer.all():
-> 3158                 raise KeyError('{} not found in axis'.format(labels))
   3159 
   3160             slicer = [slice(None)] * self.ndim

KeyError: '[] not found in axis'
```
#### Problem description

When passing a list-like to `drop`, it drops all the labels given. If there are no labels, it should drop nothing.

#### Expected Output

```python
In [8]: pd.DataFrame(index=[1,1,2]).drop([])
Out[8]: 
Empty DataFrame
Columns: []
Index: [1, 1, 2]
```

#### Output of ``pd.show_versions()``

<details>

[paste the output of ``pd.show_versions()`` here below this line]

INSTALLED VERSIONS
------------------
commit: None
python: 3.6.3.final.0
python-bits: 64
OS: Linux
OS-release: 4.13.0-45-generic
machine: x86_64
processor: x86_64
byteorder: little
LC_ALL: None
LANG: en_US.UTF-8
LOCALE: en_US.UTF-8

pandas: 0.23.0
pytest: 3.5.1
pip: 10.0.1
setuptools: 39.1.0
Cython: None
numpy: 1.14.3
scipy: None
pyarrow: 0.9.0
xarray: None
IPython: 6.2.1
sphinx: None
patsy: None
dateutil: 2.7.2
pytz: 2018.4
blosc: None
bottleneck: None
tables: None
numexpr: None
feather: 0.4.0
matplotlib: None
openpyxl: None
xlrd: None
xlwt: None
xlsxwriter: None
lxml: None
bs4: None
html5lib: None
sqlalchemy: None
pymysql: None
psycopg2: None
jinja2: None
s3fs: None
fastparquet: None
pandas_gbq: None
pandas_datareader: None

</details>



Discussion:
That does seem strange that the uniqueness would impact the behavior here. Investigation and PRs welcome!
Even worse, if some keys are missing (and some are not), ``drop`` will raise on unique indexes and not on non-unique:

``` python
In [2]: pd.DataFrame(index=[1,2,3]).drop([1, 4])
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-2-e1860fd637df> in <module>()
----> 1 pd.DataFrame(index=[1,2,3]).drop([1, 4])

/home/nobackup/repo/pandas/pandas/core/frame.py in drop(self, labels, axis, index, columns, level, inplace, errors)
   3695                                            index=index, columns=columns,
   3696                                            level=level, inplace=inplace,
-> 3697                                            errors=errors)
   3698 
   3699     @rewrite_axis_style_signature('mapper', [('copy', True),

/home/nobackup/repo/pandas/pandas/core/generic.py in drop(self, labels, axis, index, columns, level, inplace, errors)
   3106         for axis, labels in axes.items():
   3107             if labels is not None:
-> 3108                 obj = obj._drop_axis(labels, axis, level=level, errors=errors)
   3109 
   3110         if inplace:

/home/nobackup/repo/pandas/pandas/core/generic.py in _drop_axis(self, labels, axis, level, errors)
   3138                 new_axis = axis.drop(labels, level=level, errors=errors)
   3139             else:
-> 3140                 new_axis = axis.drop(labels, errors=errors)
   3141             dropped = self.reindex(**{axis_name: new_axis})
   3142             try:

/home/nobackup/repo/pandas/pandas/core/indexes/base.py in drop(self, labels, errors)
   4345             if errors != 'ignore':
   4346                 raise KeyError(
-> 4347                     'labels %s not contained in axis' % labels[mask])
   4348             indexer = indexer[~mask]
   4349         return self.delete(indexer)

KeyError: 'labels [4] not contained in axis'

In [3]: pd.DataFrame(index=[1,2,2]).drop([1, 4])
Out[3]: 
Empty DataFrame
Columns: []
Index: [2, 2]
```

I think the fix is simple: this ``.all()``:

https://github.com/pandas-dev/pandas/blob/9d61ab5aae0dd4ca3f805275c6a8ebcaeef77d64/pandas/core/generic.py#L3157

should be ``.any()``.
@toobaz mind if I take a look at this? 
@alimcmaster1 go for it
> @toobaz mind if I take a look at this?

@alimcmaster1 sure, as @WillAyd said. By the way, there is some scope for clearning (the ``set_names`` part is obsolete and can be removed).

Resolving patch:
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -3129,7 +3129,7 @@ def _drop_axis(self, labels, axis, level=None, errors='raise'):
         """
         axis = self._get_axis_number(axis)
         axis_name = self._get_axis_name(axis)
-        axis, axis_ = self._get_axis(axis), axis
+        axis = self._get_axis(axis)
 
         if axis.is_unique:
             if level is not None:
@@ -3138,24 +3138,25 @@ def _drop_axis(self, labels, axis, level=None, errors='raise'):
                 new_axis = axis.drop(labels, level=level, errors=errors)
             else:
                 new_axis = axis.drop(labels, errors=errors)
-            dropped = self.reindex(**{axis_name: new_axis})
-            try:
-                dropped.axes[axis_].set_names(axis.names, inplace=True)
-            except AttributeError:
-                pass
-            result = dropped
+            result = self.reindex(**{axis_name: new_axis})
 
+        # Case for non-unique axis
         else:
             labels = _ensure_object(com._index_labels_to_array(labels))
             if level is not None:
                 if not isinstance(axis, MultiIndex):
                     raise AssertionError('axis must be a MultiIndex')
                 indexer = ~axis.get_level_values(level).isin(labels)
+
+                # GH 18561 MultiIndex.drop should raise if label is absent
+                if errors == 'raise' and indexer.all():
+                    raise KeyError('{} not found in axis'.format(labels))
             else:
                 indexer = ~axis.isin(labels)
-
-            if errors == 'raise' and indexer.all():
-                raise KeyError('{} not found in axis'.format(labels))
+                # Check if label doesn't exist along axis
+                labels_missing = (axis.get_indexer_for(labels) == -1).any()
+                if errors == 'raise' and labels_missing:
+                    raise KeyError('{} not found in axis'.format(labels))
 
             slicer = [slice(None)] * self.ndim
             slicer[self._get_axis_number(axis_name)] = indexer
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
@@ -4341,7 +4341,7 @@ def drop(self, labels, errors='raise'):
         Raises
         ------
         KeyError
-            If none of the labels are found in the selected axis
+            If not all of the labels are found in the selected axis
         """
         arr_dtype = 'object' if self.dtype == 'object' else None
         labels = com._index_labels_to_array(labels, dtype=arr_dtype)
@@ -4350,7 +4350,7 @@ def drop(self, labels, errors='raise'):
         if mask.any():
             if errors != 'ignore':
                 raise KeyError(
-                    'labels %s not contained in axis' % labels[mask])
+                    '{} not found in axis'.format(labels[mask]))
             indexer = indexer[~mask]
         return self.delete(indexer)
 
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
@@ -1707,7 +1707,6 @@ def drop(self, labels, level=None, errors='raise'):
                 if errors != 'ignore':
                     raise ValueError('labels %s not contained in axis' %
                                      labels[mask])
-                indexer = indexer[~mask]
         except Exception:
             pass
 


Test patch:
diff --git a/pandas/tests/frame/test_axis_select_reindex.py b/pandas/tests/frame/test_axis_select_reindex.py
--- a/pandas/tests/frame/test_axis_select_reindex.py
+++ b/pandas/tests/frame/test_axis_select_reindex.py
@@ -1151,3 +1151,18 @@ def test_raise_on_drop_duplicate_index(self, actual):
         expected_no_err = actual.T.drop('c', axis=1, level=level,
                                         errors='ignore')
         assert_frame_equal(expected_no_err.T, actual)
+
+    @pytest.mark.parametrize('index', [[1, 2, 3], [1, 1, 2]])
+    @pytest.mark.parametrize('drop_labels', [[], [1], [2]])
+    def test_drop_empty_list(self, index, drop_labels):
+        # GH 21494
+        expected_index = [i for i in index if i not in drop_labels]
+        frame = pd.DataFrame(index=index).drop(drop_labels)
+        tm.assert_frame_equal(frame, pd.DataFrame(index=expected_index))
+
+    @pytest.mark.parametrize('index', [[1, 2, 3], [1, 2, 2]])
+    @pytest.mark.parametrize('drop_labels', [[1, 4], [4, 5]])
+    def test_drop_non_empty_list(self, index, drop_labels):
+        # GH 21494
+        with tm.assert_raises_regex(KeyError, 'not found in axis'):
+            pd.DataFrame(index=index).drop(drop_labels)
diff --git a/pandas/tests/series/indexing/test_alter_index.py b/pandas/tests/series/indexing/test_alter_index.py
--- a/pandas/tests/series/indexing/test_alter_index.py
+++ b/pandas/tests/series/indexing/test_alter_index.py
@@ -472,54 +472,86 @@ def test_rename():
     assert result.name == expected.name
 
 
-def test_drop():
-    # unique
-    s = Series([1, 2], index=['one', 'two'])
-    expected = Series([1], index=['one'])
-    result = s.drop(['two'])
-    assert_series_equal(result, expected)
-    result = s.drop('two', axis='rows')
-    assert_series_equal(result, expected)
-
-    # non-unique
-    # GH 5248
-    s = Series([1, 1, 2], index=['one', 'two', 'one'])
-    expected = Series([1, 2], index=['one', 'one'])
-    result = s.drop(['two'], axis=0)
-    assert_series_equal(result, expected)
-    result = s.drop('two')
-    assert_series_equal(result, expected)
-
-    expected = Series([1], index=['two'])
-    result = s.drop(['one'])
-    assert_series_equal(result, expected)
-    result = s.drop('one')
-    assert_series_equal(result, expected)
+@pytest.mark.parametrize(
+    'data, index, drop_labels,'
+    ' axis, expected_data, expected_index',
+    [
+        # Unique Index
+        ([1, 2], ['one', 'two'], ['two'],
+         0, [1], ['one']),
+        ([1, 2], ['one', 'two'], ['two'],
+         'rows', [1], ['one']),
+        ([1, 1, 2], ['one', 'two', 'one'], ['two'],
+         0, [1, 2], ['one', 'one']),
+
+        # GH 5248 Non-Unique Index
+        ([1, 1, 2], ['one', 'two', 'one'], 'two',
+         0, [1, 2], ['one', 'one']),
+        ([1, 1, 2], ['one', 'two', 'one'], ['one'],
+         0, [1], ['two']),
+        ([1, 1, 2], ['one', 'two', 'one'], 'one',
+         0, [1], ['two'])])
+def test_drop_unique_and_non_unique_index(data, index, axis, drop_labels,
+                                          expected_data, expected_index):
+
+    s = Series(data=data, index=index)
+    result = s.drop(drop_labels, axis=axis)
+    expected = Series(data=expected_data, index=expected_index)
+    tm.assert_series_equal(result, expected)
 
-    # single string/tuple-like
-    s = Series(range(3), index=list('abc'))
-    pytest.raises(KeyError, s.drop, 'bc')
-    pytest.raises(KeyError, s.drop, ('a',))
 
+@pytest.mark.parametrize(
+    'data, index, drop_labels,'
+    ' axis, error_type, error_desc',
+    [
+        # single string/tuple-like
+        (range(3), list('abc'), 'bc',
+         0, KeyError, 'not found in axis'),
+
+        # bad axis
+        (range(3), list('abc'), ('a',),
+         0, KeyError, 'not found in axis'),
+        (range(3), list('abc'), 'one',
+         'columns', ValueError, 'No axis named columns')])
+def test_drop_exception_raised(data, index, drop_labels,
+                               axis, error_type, error_desc):
+
+    with tm.assert_raises_regex(error_type, error_desc):
+        Series(data, index=index).drop(drop_labels, axis=axis)
+
+
+def test_drop_with_ignore_errors():
     # errors='ignore'
     s = Series(range(3), index=list('abc'))
     result = s.drop('bc', errors='ignore')
-    assert_series_equal(result, s)
+    tm.assert_series_equal(result, s)
     result = s.drop(['a', 'd'], errors='ignore')
     expected = s.iloc[1:]
-    assert_series_equal(result, expected)
-
-    # bad axis
-    pytest.raises(ValueError, s.drop, 'one', axis='columns')
+    tm.assert_series_equal(result, expected)
 
     # GH 8522
     s = Series([2, 3], index=[True, False])
     assert s.index.is_object()
     result = s.drop(True)
     expected = Series([3], index=[False])
-    assert_series_equal(result, expected)
+    tm.assert_series_equal(result, expected)
+
 
-    # GH 16877
-    s = Series([2, 3], index=[0, 1])
-    with tm.assert_raises_regex(KeyError, 'not contained in axis'):
-        s.drop([False, True])
+@pytest.mark.parametrize('index', [[1, 2, 3], [1, 1, 3]])
+@pytest.mark.parametrize('drop_labels', [[], [1], [3]])
+def test_drop_empty_list(index, drop_labels):
+    # GH 21494
+    expected_index = [i for i in index if i not in drop_labels]
+    series = pd.Series(index=index).drop(drop_labels)
+    tm.assert_series_equal(series, pd.Series(index=expected_index))
+
+
+@pytest.mark.parametrize('data, index, drop_labels', [
+    (None, [1, 2, 3], [1, 4]),
+    (None, [1, 2, 2], [1, 4]),
+    ([2, 3], [0, 1], [False, True])
+])
+def test_drop_non_empty_list(data, index, drop_labels):
+    # GH 21494 and GH 16877
+    with tm.assert_raises_regex(KeyError, 'not found in axis'):
+        pd.Series(data=data, index=index).drop(drop_labels)