Repository: pandas-dev/pandas
Pull request: BUG: support for "level=" when reset_index() is called with a flat Index
 - [x] closes #16263
 - [x] tests added / passed
 - [x] passes ``git diff upstream/master --name-only -- '*.py' | flake8 --diff``
 - [x] whatsnew entry

Problem statement:
DataFrame.reset_index deletes index, does not all for ints as level arg
#### Code Sample, a copy-pastable example if possible
```python
import pandas as pd
import numpy as np
pd.__version__
```
  u'0.20.1'

```python
data = pd.DataFrame(np.random.randn(4,4), columns=['A', 'B', 'C', 'D']) 
data
```

<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>A</th>
      <th>B</th>
      <th>C</th>
      <th>D</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>-0.134549</td>
      <td>2.352525</td>
      <td>0.481132</td>
      <td>-1.919506</td>
    </tr>
    <tr>
      <th>1</th>
      <td>1.980074</td>
      <td>0.720437</td>
      <td>0.410702</td>
      <td>-0.703470</td>
    </tr>
    <tr>
      <th>2</th>
      <td>-3.063166</td>
      <td>-0.781255</td>
      <td>0.270469</td>
      <td>-0.539081</td>
    </tr>
    <tr>
      <th>3</th>
      <td>-1.125265</td>
      <td>0.308374</td>
      <td>-0.166085</td>
      <td>-1.253959</td>
    </tr>
  </tbody>
</table>
</div>

```python
data.set_index(['A'], inplace=True)
data
```
<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>B</th>
      <th>C</th>
      <th>D</th>
    </tr>
    <tr>
      <th>A</th>
      <th></th>
      <th></th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>-0.134549</th>
      <td>2.352525</td>
      <td>0.481132</td>
      <td>-1.919506</td>
    </tr>
    <tr>
      <th>1.980074</th>
      <td>0.720437</td>
      <td>0.410702</td>
      <td>-0.703470</td>
    </tr>
    <tr>
      <th>-3.063166</th>
      <td>-0.781255</td>
      <td>0.270469</td>
      <td>-0.539081</td>
    </tr>
    <tr>
      <th>-1.125265</th>
      <td>0.308374</td>
      <td>-0.166085</td>
      <td>-1.253959</td>
    </tr>
  </tbody>
</table>
</div>

```python
data.reset_index(level=['A'], inplace=True)
data
```
<div>

<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>B</th>
      <th>C</th>
      <th>D</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>2.352525</td>
      <td>0.481132</td>
      <td>-1.919506</td>
    </tr>
    <tr>
      <th>1</th>
      <td>0.720437</td>
      <td>0.410702</td>
      <td>-0.703470</td>
    </tr>
    <tr>
      <th>2</th>
      <td>-0.781255</td>
      <td>0.270469</td>
      <td>-0.539081</td>
    </tr>
    <tr>
      <th>3</th>
      <td>0.308374</td>
      <td>-0.166085</td>
      <td>-1.253959</td>
    </tr>
  </tbody>
</table>
</div>


#### Problem description

between v0.19.2 and v0.20.1, the behavior of DataFrame.reset_index changed. 
With a single set index:
* It does not attempt to keep the column (essentially making drop=True always on)
* level=int no longer works (iterables work)

#### Expected Output

v0.19.2 results:

```python
data.reset_index(level=['A'], inplace=True)
data
```

<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>A</th>
      <th>B</th>
      <th>C</th>
      <th>D</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>0.100442</td>
      <td>-0.620740</td>
      <td>-2.018020</td>
      <td>1.059871</td>
    </tr>
    <tr>
      <th>1</th>
      <td>-0.530272</td>
      <td>0.402598</td>
      <td>-1.453445</td>
      <td>-0.729623</td>
    </tr>
    <tr>
      <th>2</th>
      <td>-1.040126</td>
      <td>-0.536687</td>
      <td>-1.136123</td>
      <td>-0.748891</td>
    </tr>
    <tr>
      <th>3</th>
      <td>-0.269727</td>
      <td>0.182250</td>
      <td>0.847344</td>
      <td>0.785692</td>
    </tr>
  </tbody>
</table>
</div>


#### Output of ``pd.show_versions()``

<details>

```python
import pandas as pd
import numpy as np
pd.show_versions()
```

    INSTALLED VERSIONS
    ------------------
    commit: None
    python: 2.7.13.final.0
    python-bits: 64
    OS: Darwin
    OS-release: 15.6.0
    machine: x86_64
    processor: i386
    byteorder: little
    LC_ALL: None
    LANG: en_US.UTF-8
    LOCALE: None.None
    
    pandas: 0.19.2
    nose: None
    pip: 9.0.1
    setuptools: 35.0.2
    Cython: None
    numpy: 1.12.1
    scipy: None
    statsmodels: None
    xarray: None
    IPython: 5.3.0
    sphinx: None
    patsy: None
    dateutil: 2.6.0
    pytz: 2017.2
    blosc: None
    bottleneck: None
    tables: None
    numexpr: None
    matplotlib: None
    openpyxl: None
    xlrd: None
    xlwt: None
    xlsxwriter: None
    lxml: None
    bs4: None
    html5lib: 0.999999999
    httplib2: None
    apiclient: None
    sqlalchemy: None
    pymysql: None
    psycopg2: None
    jinja2: 2.9.6
    boto: None
    pandas_datareader: None

</details>

Hints from issue discussion:
This is related to passing the ``level=`` argument when there is a non-Multi``Index``. Before, the argument would be just discarded:

```python
In [3]: pd.DataFrame(np.random.randn(4,4), columns=['A', 'B', 'C', 'D']).reset_index(level='not present')
Out[3]: 
   index         A         B         C         D
0      0  0.057457  0.065932  0.276079  0.305390
1      1 -0.562195 -0.385750 -0.228925 -0.426511
2      2  0.377559 -0.837031 -0.384840 -0.305262
3      3 -0.670057 -0.737446  0.561989  0.528754

In [4]: pd.DataFrame(np.random.randn(4,4), columns=['A', 'B', 'C', 'D']).reset_index(level=['not present'])
Out[4]: 
   index         A         B         C         D
0      0  0.613373 -0.169316 -0.592379  1.050764
1      1  0.069762  0.995308  0.030434 -0.361300
2      2 -0.526487  0.165054  0.015452  0.954447
3      3  0.585677 -1.435712 -0.298280 -0.581473

```

 but a7a0574a0bcb8a4c65ed7f5006972e24f90ae3df changed the behaviour so that now, vice-versa, even valid level names/indices are not considered.

I can provide a PR, the question is what we want to do with non-existent level names: raise or ignore?
> I can provide a PR, the question is what we want to do with non-existent level names: raise or ignore?

Sorry, the question is already answered by the behaviour when there is a ``MultiIndex``:

```python
In [5]: pd.DataFrame(np.random.randn(4,4), columns=['A', 'B', 'C', 'D']).set_index(['A', 'B']).reset_index(level=['A', 'E'])
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
/home/nobackup/repo/pandas/pandas/core/indexes/multi.py in _get_level_number(self, level)
    610                                  'level number' % level)
--> 611             level = self.names.index(level)
    612         except ValueError:

ValueError: 'E' is not in list

During handling of the above exception, another exception occurred:

KeyError                                  Traceback (most recent call last)
<ipython-input-5-f1686d7d4dfc> in <module>()
----> 1 pd.DataFrame(np.random.randn(4,4), columns=['A', 'B', 'C', 'D']).set_index(['A', 'B']).reset_index(level=['A', 'E'])

/home/nobackup/repo/pandas/pandas/core/frame.py in reset_index(self, level, drop, inplace, col_level, col_fill)
   3016             if not isinstance(level, (tuple, list)):
   3017                 level = [level]
-> 3018             level = [self.index._get_level_number(lev) for lev in level]
   3019         if isinstance(self.index, MultiIndex):
   3020             if len(level) < self.index.nlevels:

/home/nobackup/repo/pandas/pandas/core/frame.py in <listcomp>(.0)
   3016             if not isinstance(level, (tuple, list)):
   3017                 level = [level]
-> 3018             level = [self.index._get_level_number(lev) for lev in level]
   3019         if isinstance(self.index, MultiIndex):
   3020             if len(level) < self.index.nlevels:

/home/nobackup/repo/pandas/pandas/core/indexes/multi.py in _get_level_number(self, level)
    612         except ValueError:
    613             if not isinstance(level, int):
--> 614                 raise KeyError('Level %s not found' % str(level))
    615             elif level < 0:
    616                 level += self.nlevels

KeyError: 'Level E not found'

```
@m4g005 Thanks for the report! And @toobaz for the quick analysis. 
This is indeed a regression (although it seems it was more working by accident before)

Commit message: TST: test drop= and Series

Patch to review:
diff --git a/doc/source/whatsnew/v0.20.2.txt b/doc/source/whatsnew/v0.20.2.txt
index 504f8004bc8a6..f14a08876b6e8 100644
--- a/doc/source/whatsnew/v0.20.2.txt
+++ b/doc/source/whatsnew/v0.20.2.txt
@@ -42,7 +42,7 @@ Conversion
 Indexing
 ^^^^^^^^
 
-
+- Bug in ``DataFrame.reset_index(level=)`` with single level index (:issue:`16263`)
 
 
 I/O
diff --git a/pandas/core/frame.py b/pandas/core/frame.py
index e6ea58e7e05be..8d437102e4d18 100644
--- a/pandas/core/frame.py
+++ b/pandas/core/frame.py
@@ -3012,12 +3012,12 @@ def _maybe_casted_values(index, labels=None):
             return values
 
         new_index = _default_index(len(new_obj))
-        if 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):
+        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 isinstance(self.index, MultiIndex):
+                if len(level) < self.index.nlevels:
                     new_index = self.index.droplevel(level)
 
         if not drop:
@@ -3033,6 +3033,8 @@ def _maybe_casted_values(index, labels=None):
 
             multi_col = isinstance(self.columns, MultiIndex)
             for i, (lev, lab) in reversed(list(enumerate(to_insert))):
+                if not (level is None or i in level):
+                    continue
                 name = names[i]
                 if multi_col:
                     col_name = (list(name) if isinstance(name, tuple)
@@ -3049,11 +3051,9 @@ def _maybe_casted_values(index, labels=None):
                     missing = self.columns.nlevels - len(name_lst)
                     name_lst += [col_fill] * missing
                     name = tuple(name_lst)
-
                 # to ndarray and maybe infer different dtype
                 level_values = _maybe_casted_values(lev, lab)
-                if level is None or i in level:
-                    new_obj.insert(0, name, level_values)
+                new_obj.insert(0, name, level_values)
 
         new_obj.index = new_index
         if not inplace:
diff --git a/pandas/tests/frame/test_alter_axes.py b/pandas/tests/frame/test_alter_axes.py
index e6313dfc602a8..594ee72003d81 100644
--- a/pandas/tests/frame/test_alter_axes.py
+++ b/pandas/tests/frame/test_alter_axes.py
@@ -641,6 +641,43 @@ def test_reset_index(self):
         xp = xp.set_index(['B'], append=True)
         assert_frame_equal(rs, xp, check_names=False)
 
+    def test_reset_index_level(self):
+        df = pd.DataFrame([[1, 2, 3, 4], [5, 6, 7, 8]],
+                          columns=['A', 'B', 'C', 'D'])
+
+        for levels in ['A', 'B'], [0, 1]:
+            # With MultiIndex
+            result = df.set_index(['A', 'B']).reset_index(level=levels[0])
+            tm.assert_frame_equal(result, df.set_index('B'))
+
+            result = df.set_index(['A', 'B']).reset_index(level=levels[:1])
+            tm.assert_frame_equal(result, df.set_index('B'))
+
+            result = df.set_index(['A', 'B']).reset_index(level=levels)
+            tm.assert_frame_equal(result, df)
+
+            result = df.set_index(['A', 'B']).reset_index(level=levels,
+                                                          drop=True)
+            tm.assert_frame_equal(result, df[['C', 'D']])
+
+            # With flat Index (GH 16263)
+            result = df.set_index('A').reset_index(level=levels[0])
+            tm.assert_frame_equal(result, df)
+
+            result = df.set_index('A').reset_index(level=levels[:1])
+            tm.assert_frame_equal(result, df)
+
+            result = df.set_index(['A']).reset_index(level=levels[0],
+                                                     drop=True)
+            tm.assert_frame_equal(result, df[['B', 'C', 'D']])
+
+        # Missing levels - for both MultiIndex and flat Index:
+        for idx_lev in ['A', 'B'], ['A']:
+            with tm.assert_raises_regex(KeyError, 'Level E '):
+                df.set_index(idx_lev).reset_index(level=['A', 'E'])
+            with tm.assert_raises_regex(IndexError, 'Too many levels'):
+                df.set_index(idx_lev).reset_index(level=[0, 1, 2])
+
     def test_reset_index_right_dtype(self):
         time = np.arange(0.0, 10, np.sqrt(2) / 2)
         s1 = Series((9.81 * time ** 2) / 2,
diff --git a/pandas/tests/series/test_alter_axes.py b/pandas/tests/series/test_alter_axes.py
index 150767ee9e2b2..5953bc8edde2c 100644
--- a/pandas/tests/series/test_alter_axes.py
+++ b/pandas/tests/series/test_alter_axes.py
@@ -141,6 +141,45 @@ def test_reset_index(self):
         tm.assert_index_equal(rs.index, Index(index.get_level_values(1)))
         assert isinstance(rs, Series)
 
+    def test_reset_index_level(self):
+        df = pd.DataFrame([[1, 2, 3], [4, 5, 6]],
+                          columns=['A', 'B', 'C'])
+
+        for levels in ['A', 'B'], [0, 1]:
+            # With MultiIndex
+            s = df.set_index(['A', 'B'])['C']
+
+            result = s.reset_index(level=levels[0])
+            tm.assert_frame_equal(result, df.set_index('B'))
+
+            result = s.reset_index(level=levels[:1])
+            tm.assert_frame_equal(result, df.set_index('B'))
+
+            result = s.reset_index(level=levels)
+            tm.assert_frame_equal(result, df)
+
+            result = df.set_index(['A', 'B']).reset_index(level=levels,
+                                                          drop=True)
+            tm.assert_frame_equal(result, df[['C']])
+
+            with tm.assert_raises_regex(KeyError, 'Level E '):
+                s.reset_index(level=['A', 'E'])
+
+            # With flat Index
+            s = df.set_index('A')['B']
+
+            result = s.reset_index(level=levels[0])
+            tm.assert_frame_equal(result, df[['A', 'B']])
+
+            result = s.reset_index(level=levels[:1])
+            tm.assert_frame_equal(result, df[['A', 'B']])
+
+            result = s.reset_index(level=levels[0], drop=True)
+            tm.assert_series_equal(result, df['B'])
+
+            with tm.assert_raises_regex(IndexError, 'Too many levels'):
+                s.reset_index(level=[0, 1, 2])
+
     def test_reset_index_range(self):
         # GH 12071
         s = pd.Series(range(2), name='A', dtype='int64')


Code review comments:
diff_hunk: @@ -141,6 +141,45 @@ def test_reset_index(self):
         tm.assert_index_equal(rs.index, Index(index.get_level_values(1)))
         assert isinstance(rs, Series)
 
+    def test_reset_index_level(self):
+        df = pd.DataFrame([[1, 2, 3], [4, 5, 6]],
+                          columns=['A', 'B', 'C'])
+
+        for levels in ['A', 'B'], [0, 1]:
+            # With MultiIndex
+            s = df.set_index(['A', 'B'])['C']
+
+            result = s.reset_index(level=levels[0])
+            tm.assert_frame_equal(result, df.set_index('B'))
+
+            result = s.reset_index(level=levels[:1])
+            tm.assert_frame_equal(result, df.set_index('B'))
+
+            result = s.reset_index(level=levels)
+            tm.assert_frame_equal(result, df)
+
+            result = df.set_index(['A', 'B']).reset_index(level=levels,
+                                                          drop=True)
+            tm.assert_frame_equal(result, df[['C']])
+
+            with tm.assert_raises_regex(KeyError, 'Level E '):
+                s.reset_index(level=['A', 'E'])
+
+            # With flat Index
path: pandas/tests/series/test_alter_axes.py
text: @user1:
don't use word flat, rather single level