Repository: pandas-dev/pandas

Issue:
API: Add DataFrame.droplevel
As a way to make dropping an index level possible in an method chain.

```python
>>> df = pd.DataFrame(np.arange(12).reshape(4, 3), columns=['a', 'b', 'c']).set_index(['a', 'b'])
>>> df.droplevel(0)
```

roughly equivalent to 

```python
df2 = df.copy()
df2.index = df.index.droplevel(0)
df2
```

cc @twiecki 


Discussion:
@woohoo! I'd love for this to come into pandas, so that I don't have to worry about implementing it in pyjanitor! (Issue on my own package, which I'm more than willing to close if this happens: https://github.com/ericmjl/pyjanitor/issues/8)
Also, a hacky implementation to hopefully kickstart things: https://github.com/ericmjl/pyjanitor/blob/index-manipulation/janitor/functions.py#L324

and

https://github.com/ericmjl/pyjanitor/blob/index-manipulation/janitor/dataframe.py#L57
Thanks @TomAugspurger! This would be a great addition.

One could make the same case for all other Index methods that return an index besides `rename_axis` and `droplevel()`, too. I wonder if it's worth thinking about a more general solution. E.g. something along the lines of a forward of `df.index_*() -> df.index = df.index.*()`.
Not against this, but will it have any different functionality than `reset_index` with `drop=True`?

Seems like that covers what you'd want to do, unless I'm misinterpreting what this is supposed to do:
```python
In [2]: df = pd.DataFrame(np.arange(16).reshape(4, 4), columns=list('abcd')).set_index(list('abc'))

In [3]: df
Out[3]:
           d
a  b  c
0  1  2    3
4  5  6    7
8  9  10  11
12 13 14  15

In [4]: df.reset_index(level=1, drop=True)
Out[4]:
        d
a  c
0  2    3
4  6    7
8  10  11
12 14  15

In [5]: df.reset_index(level='c', drop=True)
Out[5]:
        d
a  b
0  1    3
4  5    7
8  9   11
12 13  15

In [6]: df.reset_index(level=['a', 'c'], drop=True)
Out[6]:
     d
b
1    3
5    7
9   11
13  15
```
Ah, good point. I suspect that covers all the needs.

This would be good to document in reset_index and in MultiIndex.droplevel.

On Wed, Mar 14, 2018 at 11:47 AM, jschendel <notifications@github.com>
wrote:

> Not against this, but will it have any different functionality than
> reset_index with drop=True?
>
> Seems like that covers a lot of what you'd want to do:
>
> In [2]: df = pd.DataFrame(np.arange(16).reshape(4, 4), columns=list('abcd')).set_index(list('abc'))
>
> In [3]: df
> Out[3]:
>            d
> a  b  c0  1  2    34  5  6    78  9  10  1112 13 14  15
>
> In [4]: df.reset_index(level=1, drop=True)
> Out[4]:
>         d
> a  c0  2    34  6    78  10  1112 14  15
>
> In [5]: df.reset_index(level='c', drop=True)
> Out[5]:
>         d
> a  b0  1    34  5    78  9   1112 13  15
>
> In [6]: df.reset_index(level=['a', 'c'], drop=True)
> Out[6]:
>      d
> b1    35    79   1113  15
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/pandas-dev/pandas/issues/20342#issuecomment-373092979>,
> or mute the thread
> <https://github.com/notifications/unsubscribe-auth/ABQHIg2mu7QcbSgsWAfFPM-vKhZmLflmks5teUm2gaJpZM4SqWfj>
> .
>

Don't think it totally covers everything...sometimes find a need to drop a column-index level, and i don't think reset_index supports columns (for good reason)

Resolving patch:
diff --git a/pandas/core/generic.py b/pandas/core/generic.py
--- a/pandas/core/generic.py
+++ b/pandas/core/generic.py
@@ -716,6 +716,66 @@ def swapaxes(self, axis1, axis2, copy=True):
 
         return self._constructor(new_values, *new_axes).__finalize__(self)
 
+    def droplevel(self, level, axis=0):
+        """Return DataFrame with requested index / column level(s) removed.
+
+        .. versionadded:: 0.24.0
+
+        Parameters
+        ----------
+        level : int, str, or list-like
+            If a string is given, must be the name of a level
+            If list-like, elements must be names or positional indexes
+            of levels.
+
+        axis : {0 or 'index', 1 or 'columns'}, default 0
+
+
+        Returns
+        -------
+        DataFrame.droplevel()
+
+        Examples
+        --------
+        >>> df = pd.DataFrame([
+        ...:     [1, 2, 3, 4],
+        ...:     [5, 6, 7, 8],
+        ...:     [9, 10, 11, 12]
+        ...: ]).set_index([0, 1]).rename_axis(['a', 'b'])
+
+        >>> df.columns = pd.MultiIndex.from_tuples([
+        ...:    ('c', 'e'), ('d', 'f')
+        ...:], names=['level_1', 'level_2'])
+
+        >>> df
+        level_1   c   d
+        level_2   e   f
+        a b
+        1 2      3   4
+        5 6      7   8
+        9 10    11  12
+
+        >>> df.droplevel('a')
+        level_1   c   d
+        level_2   e   f
+        b
+        2        3   4
+        6        7   8
+        10      11  12
+
+        >>> df.droplevel('level2', axis=1)
+        level_1   c   d
+        a b
+        1 2      3   4
+        5 6      7   8
+        9 10    11  12
+
+        """
+        labels = self._get_axis(axis)
+        new_labels = labels.droplevel(level)
+        result = self.set_axis(new_labels, axis=axis, inplace=False)
+        return result
+
     def pop(self, item):
         """
         Return item and drop from frame. Raise KeyError if not found.


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
@@ -1056,6 +1056,28 @@ def test_reindex_signature(self):
                               "limit", "copy", "level", "method",
                               "fill_value", "tolerance"}
 
+    def test_droplevel(self):
+        # GH20342
+        df = pd.DataFrame([
+            [1, 2, 3, 4],
+            [5, 6, 7, 8],
+            [9, 10, 11, 12]
+        ])
+        df = df.set_index([0, 1]).rename_axis(['a', 'b'])
+        df.columns = pd.MultiIndex.from_tuples([('c', 'e'), ('d', 'f')],
+                                               names=['level_1', 'level_2'])
+
+        # test that dropping of a level in index works
+        expected = df.reset_index('a', drop=True)
+        result = df.droplevel('a', axis='index')
+        assert_frame_equal(result, expected)
+
+        # test that dropping of a level in columns works
+        expected = df.copy()
+        expected.columns = pd.Index(['c', 'd'], name='level_1')
+        result = df.droplevel('level_2', axis='columns')
+        assert_frame_equal(result, expected)
+
 
 class TestIntervalIndex(object):
 
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
@@ -295,3 +295,15 @@ def test_reset_index_drop_errors(self):
         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)
+
+    def test_droplevel(self):
+        # GH20342
+        ser = pd.Series([1, 2, 3, 4])
+        ser.index = pd.MultiIndex.from_arrays([(1, 2, 3, 4), (5, 6, 7, 8)],
+                                              names=['a', 'b'])
+        expected = ser.reset_index('b', drop=True)
+        result = ser.droplevel('b', axis='index')
+        assert_series_equal(result, expected)
+        # test that droplevel raises ValueError on axis != 0
+        with pytest.raises(ValueError):
+            ser.droplevel(1, axis='columns')