Repository: pandas-dev/pandas
Pull request: BUG: .reset_index() should raise with an invalid level name (GH20925)
#20925 Raises appropriate error for Series.reset_index(level_name, drop=True) when index is flat and an invalid level is supplied

- [x] closes #20925 
- [x] tests added / passed
- [x] passes `git diff upstream/master -u -- "*.py" | flake8 --diff`
- [x] whatsnew entry

Problem statement:
Series.reset_index(level_name, drop=True) accepts invalid name when index is flat
#### Code Sample, a copy-pastable example if possible

```python
In [2]: s = pd.Series(range(4))

In [3]: s.reset_index('wrong', drop=True)
Out[3]: 
0    0
1    1
2    2
3    3
dtype: int64

In [4]: s.reset_index('wrong')
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-4-a7b872dfe0cf> in <module>()
----> 1 s.reset_index('wrong')

~/nobackup/repo/pandas/pandas/core/series.py in reset_index(self, level, drop, name, inplace)
   1215         else:
   1216             df = self.to_frame(name)
-> 1217             return df.reset_index(level=level, drop=drop)
   1218 
   1219     def __unicode__(self):

~/nobackup/repo/pandas/pandas/core/frame.py in reset_index(self, level, drop, inplace, col_level, col_fill)
   4098             if not isinstance(level, (tuple, list)):
   4099                 level = [level]
-> 4100             level = [self.index._get_level_number(lev) for lev in level]
   4101             if isinstance(self.index, MultiIndex):
   4102                 if len(level) < self.index.nlevels:

~/nobackup/repo/pandas/pandas/core/frame.py in <listcomp>(.0)
   4098             if not isinstance(level, (tuple, list)):
   4099                 level = [level]
-> 4100             level = [self.index._get_level_number(lev) for lev in level]
   4101             if isinstance(self.index, MultiIndex):
   4102                 if len(level) < self.index.nlevels:

~/nobackup/repo/pandas/pandas/core/indexes/base.py in _get_level_number(self, level)
   1944 
   1945     def _get_level_number(self, level):
-> 1946         self._validate_index_level(level)
   1947         return 0
   1948 

~/nobackup/repo/pandas/pandas/core/indexes/base.py in _validate_index_level(self, level)
   1941         elif level != self.name:
   1942             raise KeyError('Level %s must be same as name (%s)' %
-> 1943                            (level, self.name))
   1944 
   1945     def _get_level_number(self, level):

KeyError: 'Level wrong must be same as name (None)'

In [5]: s = pd.Series(range(4), index=pd.MultiIndex.from_product([[1, 2]]*2))

In [6]: s.reset_index('wrong', drop=True)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
~/nobackup/repo/pandas/pandas/core/indexes/multi.py in _get_level_number(self, level)
    775                                  'level number' % level)
--> 776             level = self.names.index(level)
    777         except ValueError:

ValueError: 'wrong' is not in list

During handling of the above exception, another exception occurred:

KeyError                                  Traceback (most recent call last)
<ipython-input-6-e0d143f488d8> in <module>()
----> 1 s.reset_index('wrong', drop=True)

~/nobackup/repo/pandas/pandas/core/series.py in reset_index(self, level, drop, name, inplace)
   1199                 if not isinstance(level, (tuple, list)):
   1200                     level = [level]
-> 1201                 level = [self.index._get_level_number(lev) for lev in level]
   1202                 if len(level) < len(self.index.levels):
   1203                     new_index = self.index.droplevel(level)

~/nobackup/repo/pandas/pandas/core/series.py in <listcomp>(.0)
   1199                 if not isinstance(level, (tuple, list)):
   1200                     level = [level]
-> 1201                 level = [self.index._get_level_number(lev) for lev in level]
   1202                 if len(level) < len(self.index.levels):
   1203                     new_index = self.index.droplevel(level)

~/nobackup/repo/pandas/pandas/core/indexes/multi.py in _get_level_number(self, level)
    777         except ValueError:
    778             if not isinstance(level, int):
--> 779                 raise KeyError('Level %s not found' % str(level))
    780             elif level < 0:
    781                 level += self.nlevels

KeyError: 'Level wrong not found'
```

#### Problem description

``In [3]:`` should raise too.

#### Expected Output

Same as ``In [4]:``

#### Output of ``pd.show_versions()``

<details>

INSTALLED VERSIONS
------------------
commit: None
python: 3.5.3.final.0
python-bits: 64
OS: Linux
OS-release: 4.9.0-6-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+825.ga3facfc55
pytest: 3.5.0
pip: 9.0.1
setuptools: 39.0.1
Cython: 0.25.2
numpy: 1.14.1
scipy: 0.19.0
pyarrow: None
xarray: None
IPython: 6.2.1
sphinx: 1.5.6
patsy: 0.5.0
dateutil: 2.7.0
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>

Hints from issue discussion:
Can I try to deal with this one as my first issue?
> Can I try to deal with this one as my first issue?

Sure!

Notice the following should also raise:

``` python
In [2]: s = pd.Series(range(4), name='valid')

In [3]: s.reset_index(['valid', 'valid'], drop=True)
Out[3]: 
0    0
1    1
2    2
3    3
Name: valid, dtype: int64

```
... and bonus points if the following raises too (but leaving it aside for the moment is also fine):
``` python
In [2]: s = pd.Series(range(4))

In [3]: s.reset_index([None, None])
Out[3]: 
   index  0
0      0  0
1      1  1
2      2  2
3      3  3
```

Hi, I'm a beginner and would like to contribute as my first bug fix. I'm not quite certain I understand the bug. Could you please explain?
@rahulgo this bug is probably already being worked on by @misupova : I suggest you pick another one (still marked as [good first issue](https://github.com/pandas-dev/pandas/labels/good%20first%20issue) ), possibly one for which you have a better understanding
@misupova sorry, since this issue was open since some time I thought I'd give it a try.
> this issue was open since some time

@KalyanGokhale an 11 days old bug is open "since some time"?! Have you seen the list?!

@misupova ... hope you weren't already working on this, and that you can find another "good first issue" that suits you
Apologies @toobaz @misupova ...really not sure what the protocol is in terms of wait times / when you can start working on something or not....will be more mindful about it in future.
@toobaz I started working on that, but what’s done is done :)
@misupova sincere apologies once again.

@toobaz may be I was quick to jump on this one - but wanted to understand for future if there is any protocol you guys follow (e.g. if x # of days have passed and the person who signed up to work on an issue has not provided any update, then its fair play for someone else to pick it up etc?) - is there a middle ground between the time an issue is open and someone who has a (ready/near ready) solution can also post it....may be there is another collaborative way which I have not considered....just thinking out loud. Thanks!
@KalyanGokhale no official protocol (we have so many bugs we don't typically fight over them ;-) ), and might depend on how urgent the issue is, but I guess a rule of thumb could be "always feel free to ask if the person who signed up to work on an issue is actually/still working on it, and if s/he isn't, or doesn't reply within a week, then feel free to steal it". (and in general, when you start working on an issue, always mention it)

Commit message: Update v0.23.1.txt

Patch to review:
diff --git a/doc/source/whatsnew/v0.23.1.txt b/doc/source/whatsnew/v0.23.1.txt
index 35d150dc263b8..0b07570607731 100644
--- a/doc/source/whatsnew/v0.23.1.txt
+++ b/doc/source/whatsnew/v0.23.1.txt
@@ -59,7 +59,7 @@ Conversion
 Indexing
 ^^^^^^^^
 
--
+- Bug in :meth:`Series.reset_index` where appropriate error was not raised with a non-named level (:issue:`20925`)
 -
 
 I/O
diff --git a/pandas/core/series.py b/pandas/core/series.py
index 0e2ae22f35af7..6d396e845219e 100644
--- a/pandas/core/series.py
+++ b/pandas/core/series.py
@@ -1195,12 +1195,13 @@ def reset_index(self, level=None, drop=False, name=None, inplace=False):
         inplace = validate_bool_kwarg(inplace, 'inplace')
         if drop:
             new_index = com._default_index(len(self))
-            if level is not None and isinstance(self.index, MultiIndex):
+            if level is not None:
                 if not isinstance(level, (tuple, list)):
                     level = [level]
                 level = [self.index._get_level_number(lev) for lev in level]
-                if len(level) < len(self.index.levels):
-                    new_index = self.index.droplevel(level)
+                if isinstance(self.index, MultiIndex):
+                    if len(level) < self.index.nlevels:
+                        new_index = self.index.droplevel(level)
 
             if inplace:
                 self.index = new_index
diff --git a/pandas/tests/series/test_alter_axes.py b/pandas/tests/series/test_alter_axes.py
index dce4e82cbdcf1..859082a7e722d 100644
--- a/pandas/tests/series/test_alter_axes.py
+++ b/pandas/tests/series/test_alter_axes.py
@@ -188,6 +188,11 @@ def test_reset_index_level(self):
             with tm.assert_raises_regex(IndexError, 'Too many levels'):
                 s.reset_index(level=[0, 1, 2])
 
+        # Check that .reset_index([],drop=True) doesn't fail
+        result = pd.Series(range(4)).reset_index([], drop=True)
+        expected = pd.Series(range(4))
+        assert_series_equal(result, expected)
+
     def test_reset_index_range(self):
         # GH 12071
         s = pd.Series(range(2), name='A', dtype='int64')
@@ -275,3 +280,18 @@ def test_set_axis_prior_to_deprecation_signature(self):
             with tm.assert_produces_warning(FutureWarning):
                 result = s.set_axis(0, list('abcd'), inplace=False)
             tm.assert_series_equal(result, expected)
+
+    def test_reset_index_drop_errors(self):
+        #  GH 20925
+
+        # KeyError raised for series index when passed level name is missing
+        s = pd.Series(range(4))
+        with tm.assert_raises_regex(KeyError, 'must be same as name'):
+            s.reset_index('wrong', drop=True)
+        with tm.assert_raises_regex(KeyError, 'must be same as name'):
+            s.reset_index('wrong')
+
+        # KeyError raised for series when level to be dropped is missing
+        s = pd.Series(range(4), index=pd.MultiIndex.from_product([[1, 2]] * 2))
+        with tm.assert_raises_regex(KeyError, 'not found'):
+            s.reset_index('wrong', drop=True)


Code review comments:
diff_hunk: @@ -59,7 +59,7 @@ Conversion
 Indexing
 ^^^^^^^^
 
--
+- Bug in :meth:`Series.reset_index` where appropriate error was not raised with a non-named level (:issue:`20925`)
path: doc/source/whatsnew/v0.23.1.txt
text: @user1:
I don't get the "a non-named level". Wouldn't it be "an invalid level name"?

@author:
Agreed. Done.