From b46d46f869c35a8f50b71e837dd98db80b65f32b Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Tue, 10 Jul 2018 19:43:46 -0400 Subject: [PATCH 01/10] API: Add DataFrame.droplevel --- pandas/core/frame.py | 54 +++++++++++++++++++++++++++ pandas/tests/frame/test_alter_axes.py | 22 +++++++++++ 2 files changed, 76 insertions(+) diff --git a/pandas/core/frame.py b/pandas/core/frame.py index 4e6ddf64145a8..ee17444f589e2 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -4630,6 +4630,60 @@ def sortlevel(self, level=0, axis=0, ascending=True, inplace=False, return self.sort_index(level=level, axis=axis, ascending=ascending, inplace=inplace, sort_remaining=sort_remaining) + def droplevel(self, level, axis=0): + """Return DataFrame with requested index / column level(s) removed. + + Parameters + ---------- + level : int + axis : {0 or 'index', 1 or 'columns'}, default 0 + + .. versionadded:: 0.24.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 + + """ + result = self.copy() + axis = self._get_axis_number(axis) + if axis == 0: + result.index = result.index.droplevel(level) + else: + result.columns = result.columns.droplevel(level) + return result + def nlargest(self, n, columns, keep='first'): """ Return the first `n` rows ordered by `columns` in descending order. diff --git a/pandas/tests/frame/test_alter_axes.py b/pandas/tests/frame/test_alter_axes.py index 21961906c39bb..d0cc40012495f 100644 --- 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): + 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 the index works + expected_df_no_level_a_in_index = df.reset_index('a', drop=True) + actual_df_no_level_a_in_index = df.droplevel('a') + assert_frame_equal(expected_df_no_level_a_in_index, actual_df_no_level_a_in_index) + + # test that dropping of a level in the index works + expected_df_no_level_2_in_columns = df.copy() + expected_df_no_level_2_in_columns.columns = pd.Index(['c', 'd'], name='level_1') + actual_df_no_level_2_in_columns = df.droplevel('level_2', axis=1) + assert_frame_equal(expected_df_no_level_2_in_columns, + actual_df_no_level_2_in_columns) + class TestIntervalIndex(object): From 8836d3f353d8321beeef9504f85c0744c7ac7eab Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Thu, 12 Jul 2018 01:48:41 +0000 Subject: [PATCH 02/10] better docstring --- pandas/core/frame.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pandas/core/frame.py b/pandas/core/frame.py index ee17444f589e2..b628422f9df9b 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -4635,7 +4635,10 @@ def droplevel(self, level, axis=0): Parameters ---------- - level : int + 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 indexes of levels. + axis : {0 or 'index', 1 or 'columns'}, default 0 .. versionadded:: 0.24.0 From 7862326b8d5e8d2a32b7bbcc88a4ed135a949408 Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Thu, 12 Jul 2018 01:53:57 +0000 Subject: [PATCH 03/10] pep-8 --- pandas/tests/frame/test_alter_axes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pandas/tests/frame/test_alter_axes.py b/pandas/tests/frame/test_alter_axes.py index d0cc40012495f..3d50ce1e78844 100644 --- a/pandas/tests/frame/test_alter_axes.py +++ b/pandas/tests/frame/test_alter_axes.py @@ -1069,11 +1069,13 @@ def test_droplevel(self): # test that dropping of a level in the index works expected_df_no_level_a_in_index = df.reset_index('a', drop=True) actual_df_no_level_a_in_index = df.droplevel('a') - assert_frame_equal(expected_df_no_level_a_in_index, actual_df_no_level_a_in_index) + assert_frame_equal(expected_df_no_level_a_in_index, + actual_df_no_level_a_in_index) # test that dropping of a level in the index works expected_df_no_level_2_in_columns = df.copy() - expected_df_no_level_2_in_columns.columns = pd.Index(['c', 'd'], name='level_1') + expected_df_no_level_2_in_columns.columns = pd.Index(['c', 'd'], + name='level_1') actual_df_no_level_2_in_columns = df.droplevel('level_2', axis=1) assert_frame_equal(expected_df_no_level_2_in_columns, actual_df_no_level_2_in_columns) From c43d261df56aa69754ac3f2e66c97ce6eba718d2 Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Thu, 12 Jul 2018 21:16:38 +0000 Subject: [PATCH 04/10] address comments --- doc/source/api.rst | 2 + doc/source/whatsnew/v0.24.0.txt | 1 + pandas/core/frame.py | 57 --------------------------- pandas/core/generic.py | 56 ++++++++++++++++++++++++++ pandas/tests/frame/test_alter_axes.py | 10 ++++- 5 files changed, 67 insertions(+), 59 deletions(-) diff --git a/doc/source/api.rst b/doc/source/api.rst index fff944651588e..9faac4c616477 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -444,6 +444,7 @@ Reindexing / Selection / Label manipulation Series.align Series.drop + Series.droplevel Series.drop_duplicates Series.duplicated Series.equals @@ -1063,6 +1064,7 @@ Reshaping, sorting, transposing .. autosummary:: :toctree: generated/ + DataFrame.droplevel DataFrame.pivot DataFrame.pivot_table DataFrame.reorder_levels diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 034a56b2ac0cb..18bdc649bb6f6 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -74,6 +74,7 @@ Other Enhancements - :func:`Series.mode` and :func:`DataFrame.mode` now support the ``dropna`` parameter which can be used to specify whether NaN/NaT values should be considered (:issue:`17534`) - :func:`to_csv` now supports ``compression`` keyword when a file handle is passed. (:issue:`21227`) - :meth:`Index.droplevel` is now implemented also for flat indexes, for compatibility with :class:`MultiIndex` (:issue:`21115`) +- :meth:`Series.droplevel` and `DataFrame.droplevel` are now implemented - Added support for reading from Google Cloud Storage via the ``gcsfs`` library (:issue:`19454`) - :func:`to_gbq` and :func:`read_gbq` signature and documentation updated to reflect changes from the `Pandas-GBQ library version 0.5.0 diff --git a/pandas/core/frame.py b/pandas/core/frame.py index b628422f9df9b..4e6ddf64145a8 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -4630,63 +4630,6 @@ def sortlevel(self, level=0, axis=0, ascending=True, inplace=False, return self.sort_index(level=level, axis=axis, ascending=ascending, inplace=inplace, sort_remaining=sort_remaining) - def droplevel(self, level, axis=0): - """Return DataFrame with requested index / column level(s) removed. - - 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 indexes of levels. - - axis : {0 or 'index', 1 or 'columns'}, default 0 - - .. versionadded:: 0.24.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 - - """ - result = self.copy() - axis = self._get_axis_number(axis) - if axis == 0: - result.index = result.index.droplevel(level) - else: - result.columns = result.columns.droplevel(level) - return result - def nlargest(self, n, columns, keep='first'): """ Return the first `n` rows ordered by `columns` in descending order. diff --git a/pandas/core/generic.py b/pandas/core/generic.py index 8da678e0adec0..bf142272de2bb 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -716,6 +716,62 @@ 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 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. diff --git a/pandas/tests/frame/test_alter_axes.py b/pandas/tests/frame/test_alter_axes.py index 3d50ce1e78844..6b5425f9c12c8 100644 --- a/pandas/tests/frame/test_alter_axes.py +++ b/pandas/tests/frame/test_alter_axes.py @@ -1063,16 +1063,17 @@ def test_droplevel(self): [9, 10, 11, 12] ]) df = df.set_index([0, 1]).rename_axis(['a', 'b']) + ser = df.iloc[:, 0] df.columns = pd.MultiIndex.from_tuples([('c', 'e'), ('d', 'f')], names=['level_1', 'level_2']) - # test that dropping of a level in the index works + # test that dropping of a level in DataFrame index works expected_df_no_level_a_in_index = df.reset_index('a', drop=True) actual_df_no_level_a_in_index = df.droplevel('a') assert_frame_equal(expected_df_no_level_a_in_index, actual_df_no_level_a_in_index) - # test that dropping of a level in the index works + # test that dropping of a level in DataFrame columns works expected_df_no_level_2_in_columns = df.copy() expected_df_no_level_2_in_columns.columns = pd.Index(['c', 'd'], name='level_1') @@ -1080,6 +1081,11 @@ def test_droplevel(self): assert_frame_equal(expected_df_no_level_2_in_columns, actual_df_no_level_2_in_columns) + # test that dropping of a level in Series index works + expected_ser_no_level_b_in_index = ser.reset_index('b', drop=True) + actual_ser_no_level_b_in_index = ser.droplevel('b') + assert_series_equal(expected_ser_no_level_b_in_index, actual_ser_no_level_b_in_index) + class TestIntervalIndex(object): From 95336db79b9bf7a1888e5f08fe1484c6578e7f1a Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Fri, 13 Jul 2018 14:28:16 +0000 Subject: [PATCH 05/10] add comment to the test --- pandas/tests/frame/test_alter_axes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/tests/frame/test_alter_axes.py b/pandas/tests/frame/test_alter_axes.py index 6b5425f9c12c8..57f43623b717f 100644 --- a/pandas/tests/frame/test_alter_axes.py +++ b/pandas/tests/frame/test_alter_axes.py @@ -1057,6 +1057,7 @@ def test_reindex_signature(self): "fill_value", "tolerance"} def test_droplevel(self): + # GH20342 df = pd.DataFrame([ [1, 2, 3, 4], [5, 6, 7, 8], From 5f9c648d30683fc9662b5ffb72fb30379e2ceaaa Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Fri, 13 Jul 2018 18:54:58 +0000 Subject: [PATCH 06/10] lint --- pandas/tests/frame/test_alter_axes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/tests/frame/test_alter_axes.py b/pandas/tests/frame/test_alter_axes.py index 57f43623b717f..fad837b3d5716 100644 --- a/pandas/tests/frame/test_alter_axes.py +++ b/pandas/tests/frame/test_alter_axes.py @@ -1085,7 +1085,8 @@ def test_droplevel(self): # test that dropping of a level in Series index works expected_ser_no_level_b_in_index = ser.reset_index('b', drop=True) actual_ser_no_level_b_in_index = ser.droplevel('b') - assert_series_equal(expected_ser_no_level_b_in_index, actual_ser_no_level_b_in_index) + assert_series_equal(expected_ser_no_level_b_in_index, + actual_ser_no_level_b_in_index) class TestIntervalIndex(object): From 808bc165f46cab8ab2b20a700b6a6380ad3c4258 Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Sat, 14 Jul 2018 15:15:22 +0000 Subject: [PATCH 07/10] address comments --- doc/source/whatsnew/v0.24.0.txt | 2 +- pandas/core/generic.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index 18bdc649bb6f6..f4acfee663548 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -74,7 +74,7 @@ Other Enhancements - :func:`Series.mode` and :func:`DataFrame.mode` now support the ``dropna`` parameter which can be used to specify whether NaN/NaT values should be considered (:issue:`17534`) - :func:`to_csv` now supports ``compression`` keyword when a file handle is passed. (:issue:`21227`) - :meth:`Index.droplevel` is now implemented also for flat indexes, for compatibility with :class:`MultiIndex` (:issue:`21115`) -- :meth:`Series.droplevel` and `DataFrame.droplevel` are now implemented +- :meth:`Series.droplevel` and `DataFrame.droplevel` are now implemented (:issue:`20342`) - Added support for reading from Google Cloud Storage via the ``gcsfs`` library (:issue:`19454`) - :func:`to_gbq` and :func:`read_gbq` signature and documentation updated to reflect changes from the `Pandas-GBQ library version 0.5.0 diff --git a/pandas/core/generic.py b/pandas/core/generic.py index bf142272de2bb..d8005a2e66b72 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -718,13 +718,14 @@ def swapaxes(self, axis1, axis2, copy=True): 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 indexes of levels. + If list-like, elements must be names or positional indexes of levels. axis : {0 or 'index', 1 or 'columns'}, default 0 From dbc25dafcbb892148ec215e522525dcddde9112a Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Sat, 14 Jul 2018 15:34:23 +0000 Subject: [PATCH 08/10] move tests for series to a dedicated file --- pandas/tests/frame/test_alter_axes.py | 30 +++++++++----------------- pandas/tests/series/test_alter_axes.py | 12 +++++++++++ 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/pandas/tests/frame/test_alter_axes.py b/pandas/tests/frame/test_alter_axes.py index fad837b3d5716..4f95eb3fe7b47 100644 --- a/pandas/tests/frame/test_alter_axes.py +++ b/pandas/tests/frame/test_alter_axes.py @@ -1064,29 +1064,19 @@ def test_droplevel(self): [9, 10, 11, 12] ]) df = df.set_index([0, 1]).rename_axis(['a', 'b']) - ser = df.iloc[:, 0] df.columns = pd.MultiIndex.from_tuples([('c', 'e'), ('d', 'f')], names=['level_1', 'level_2']) - # test that dropping of a level in DataFrame index works - expected_df_no_level_a_in_index = df.reset_index('a', drop=True) - actual_df_no_level_a_in_index = df.droplevel('a') - assert_frame_equal(expected_df_no_level_a_in_index, - actual_df_no_level_a_in_index) - - # test that dropping of a level in DataFrame columns works - expected_df_no_level_2_in_columns = df.copy() - expected_df_no_level_2_in_columns.columns = pd.Index(['c', 'd'], - name='level_1') - actual_df_no_level_2_in_columns = df.droplevel('level_2', axis=1) - assert_frame_equal(expected_df_no_level_2_in_columns, - actual_df_no_level_2_in_columns) - - # test that dropping of a level in Series index works - expected_ser_no_level_b_in_index = ser.reset_index('b', drop=True) - actual_ser_no_level_b_in_index = ser.droplevel('b') - assert_series_equal(expected_ser_no_level_b_in_index, - actual_ser_no_level_b_in_index) + # 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 index 859082a7e722d..840c80d6775a5 100644 --- 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') From 2333f7c9a264f76aa4350b6a366a0bcbd289ec4c Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Sat, 14 Jul 2018 17:08:33 +0000 Subject: [PATCH 09/10] lint --- pandas/core/generic.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pandas/core/generic.py b/pandas/core/generic.py index d8005a2e66b72..eecc235e4cb48 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -725,7 +725,8 @@ def droplevel(self, level, axis=0): ---------- 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. + If list-like, elements must be names or positional indexes + of levels. axis : {0 or 'index', 1 or 'columns'}, default 0 From 6b8f22bbf3fe2a7569a596819d08c1b8b9c33365 Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Thu, 19 Jul 2018 13:17:28 +0000 Subject: [PATCH 10/10] address minor comments --- doc/source/whatsnew/v0.24.0.txt | 2 +- pandas/core/generic.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v0.24.0.txt b/doc/source/whatsnew/v0.24.0.txt index f4acfee663548..d300c2b273906 100644 --- a/doc/source/whatsnew/v0.24.0.txt +++ b/doc/source/whatsnew/v0.24.0.txt @@ -74,7 +74,7 @@ Other Enhancements - :func:`Series.mode` and :func:`DataFrame.mode` now support the ``dropna`` parameter which can be used to specify whether NaN/NaT values should be considered (:issue:`17534`) - :func:`to_csv` now supports ``compression`` keyword when a file handle is passed. (:issue:`21227`) - :meth:`Index.droplevel` is now implemented also for flat indexes, for compatibility with :class:`MultiIndex` (:issue:`21115`) -- :meth:`Series.droplevel` and `DataFrame.droplevel` are now implemented (:issue:`20342`) +- :meth:`Series.droplevel` and :meth:`DataFrame.droplevel` are now implemented (:issue:`20342`) - Added support for reading from Google Cloud Storage via the ``gcsfs`` library (:issue:`19454`) - :func:`to_gbq` and :func:`read_gbq` signature and documentation updated to reflect changes from the `Pandas-GBQ library version 0.5.0 diff --git a/pandas/core/generic.py b/pandas/core/generic.py index eecc235e4cb48..608eebd079eef 100644 --- a/pandas/core/generic.py +++ b/pandas/core/generic.py @@ -742,9 +742,11 @@ def droplevel(self, level, axis=0): ...: [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