From edfb487d0d27ab53d10e8aa4a41a1c9fd0f2c183 Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Sat, 1 Oct 2016 20:59:37 -0400 Subject: [PATCH 01/13] Added failing test case of GH 14327 --- pandas/tests/test_groupby.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pandas/tests/test_groupby.py b/pandas/tests/test_groupby.py index 02917ab18c29f..3163bf61de625 100644 --- a/pandas/tests/test_groupby.py +++ b/pandas/tests/test_groupby.py @@ -458,6 +458,28 @@ def test_grouper_creation_bug(self): expected = s.groupby(level='one').sum() assert_series_equal(result, expected) + def test_grouper_column_and_index(self): + # GH 14327 + + # Grouping a multi-index frame by a column and an index level should + # be equivalent to resetting the index and grouping by two columns + idx = pd.MultiIndex.from_tuples([('a', 1), ('a', 2), ('a', 3), + ('b', 1), ('b', 2), ('b', 3)]) + idx.names = ['outer', 'inner'] + df_multi = pd.DataFrame({"A": np.arange(6), + 'B': ['one', 'one', 'two', + 'two', 'one', 'one']}, + index=idx) + result = df_multi.groupby(['B', pd.Grouper(level='inner')]).mean() + expected = df_multi.reset_index().groupby(['B', 'inner']).mean() + assert_frame_equal(result, expected) + + # Grouping a single-index frame by a column and the index should + # be equivalent to resetting the index and grouping by two columns + df_single = df_multi.reset_index('outer') + result = df_single.groupby(['B', pd.Grouper(level='inner')]).mean() + assert_frame_equal(result, expected) + def test_grouper_getting_correct_binner(self): # GH 10063 From 2c8d4227caba5bad03578e71fb0bbfc65bd83b8d Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Sat, 1 Oct 2016 21:05:41 -0400 Subject: [PATCH 02/13] Handle specification of level for non-MultiIndex in Grouping constructor Existing logic under "if level is not None:" assumed that index was a MultiIndex. Now we check and also handle the case where an Index is passed in with a None grouper. This resolves GH 14327 --- pandas/core/groupby.py | 53 ++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/pandas/core/groupby.py b/pandas/core/groupby.py index 3c376e3188eac..feee0d73e05df 100644 --- a/pandas/core/groupby.py +++ b/pandas/core/groupby.py @@ -2201,36 +2201,45 @@ def __init__(self, index, grouper=None, obj=None, name=None, level=None, raise AssertionError('Level %s not in index' % str(level)) level = index.names.index(level) - inds = index.labels[level] - level_index = index.levels[level] - if self.name is None: self.name = index.names[level] - # XXX complete hack + if isinstance(index, MultiIndex): + inds = index.labels[level] + level_index = index.levels[level] - if grouper is not None: - level_values = index.levels[level].take(inds) - self.grouper = level_values.map(self.grouper) - else: - # all levels may not be observed - labels, uniques = algos.factorize(inds, sort=True) + # XXX complete hack + + if grouper is not None: + level_values = index.levels[level].take(inds) + self.grouper = level_values.map(self.grouper) + else: + # all levels may not be observed + labels, uniques = algos.factorize(inds, sort=True) - if len(uniques) > 0 and uniques[0] == -1: - # handle NAs - mask = inds != -1 - ok_labels, uniques = algos.factorize(inds[mask], sort=True) + if len(uniques) > 0 and uniques[0] == -1: + # handle NAs + mask = inds != -1 + ok_labels, uniques = algos.factorize(inds[mask], + sort=True) - labels = np.empty(len(inds), dtype=inds.dtype) - labels[mask] = ok_labels - labels[~mask] = -1 + labels = np.empty(len(inds), dtype=inds.dtype) + labels[mask] = ok_labels + labels[~mask] = -1 - if len(uniques) < len(level_index): - level_index = level_index.take(uniques) + if len(uniques) < len(level_index): + level_index = level_index.take(uniques) + + self._labels = labels + self._group_index = level_index + self.grouper = level_index.take(labels) + + # Single level index passed + else: + # Use single level index as grouper if none passed + if grouper is None: + self.grouper = index - self._labels = labels - self._group_index = level_index - self.grouper = level_index.take(labels) else: if isinstance(self.grouper, (list, tuple)): self.grouper = com._asarray_tuplesafe(self.grouper) From ab6f9a91956e1f1652573c20c4ae8a84079c2ec3 Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Sat, 1 Oct 2016 21:26:06 -0400 Subject: [PATCH 03/13] Release notes for fix to GH 14327 --- doc/source/whatsnew/v0.19.0.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v0.19.0.txt b/doc/source/whatsnew/v0.19.0.txt index 8e7e95c071ea4..4fbf5afdf05ca 100644 --- a/doc/source/whatsnew/v0.19.0.txt +++ b/doc/source/whatsnew/v0.19.0.txt @@ -1563,3 +1563,4 @@ Bug Fixes - ``PeriodIndex`` can now accept ``list`` and ``array`` which contains ``pd.NaT`` (:issue:`13430`) - Bug in ``df.groupby`` where ``.median()`` returns arbitrary values if grouped dataframe contains empty bins (:issue:`13629`) - Bug in ``Index.copy()`` where ``name`` parameter was ignored (:issue:`14302`) +- Bug in ``df.groupby`` causing an ``AttributeError`` when grouping a single index frame by a column and the index (:issue`14327`) From 3c6bd8b5f88d45aae4c8a117f3a74a5df46203a7 Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Mon, 3 Oct 2016 15:29:03 -0400 Subject: [PATCH 04/13] Moved whatsnew to 0.20.0 --- doc/source/whatsnew/v0.19.0.txt | 1 - doc/source/whatsnew/v0.20.0.txt | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v0.19.0.txt b/doc/source/whatsnew/v0.19.0.txt index 4fbf5afdf05ca..8e7e95c071ea4 100644 --- a/doc/source/whatsnew/v0.19.0.txt +++ b/doc/source/whatsnew/v0.19.0.txt @@ -1563,4 +1563,3 @@ Bug Fixes - ``PeriodIndex`` can now accept ``list`` and ``array`` which contains ``pd.NaT`` (:issue:`13430`) - Bug in ``df.groupby`` where ``.median()`` returns arbitrary values if grouped dataframe contains empty bins (:issue:`13629`) - Bug in ``Index.copy()`` where ``name`` parameter was ignored (:issue:`14302`) -- Bug in ``df.groupby`` causing an ``AttributeError`` when grouping a single index frame by a column and the index (:issue`14327`) diff --git a/doc/source/whatsnew/v0.20.0.txt b/doc/source/whatsnew/v0.20.0.txt index 0354a8046e873..a556d8707a21d 100644 --- a/doc/source/whatsnew/v0.20.0.txt +++ b/doc/source/whatsnew/v0.20.0.txt @@ -79,3 +79,4 @@ Performance Improvements Bug Fixes ~~~~~~~~~ +- Bug in ``df.groupby`` causing an ``AttributeError`` when grouping a single index frame by a column and the index (:issue`14327`) From 6005102f40281e3dbb8c73bd311432d117bd4bf9 Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Wed, 5 Oct 2016 07:55:11 -0400 Subject: [PATCH 05/13] Moved grouper level handling logic to methods on Index/MultiIndex (GH14327) --- pandas/core/groupby.py | 37 +------------------------------------ pandas/indexes/base.py | 7 +++++++ pandas/indexes/multi.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 36 deletions(-) diff --git a/pandas/core/groupby.py b/pandas/core/groupby.py index feee0d73e05df..cc9c89702a0f1 100644 --- a/pandas/core/groupby.py +++ b/pandas/core/groupby.py @@ -2204,42 +2204,7 @@ def __init__(self, index, grouper=None, obj=None, name=None, level=None, if self.name is None: self.name = index.names[level] - if isinstance(index, MultiIndex): - inds = index.labels[level] - level_index = index.levels[level] - - # XXX complete hack - - if grouper is not None: - level_values = index.levels[level].take(inds) - self.grouper = level_values.map(self.grouper) - else: - # all levels may not be observed - labels, uniques = algos.factorize(inds, sort=True) - - if len(uniques) > 0 and uniques[0] == -1: - # handle NAs - mask = inds != -1 - ok_labels, uniques = algos.factorize(inds[mask], - sort=True) - - labels = np.empty(len(inds), dtype=inds.dtype) - labels[mask] = ok_labels - labels[~mask] = -1 - - if len(uniques) < len(level_index): - level_index = level_index.take(uniques) - - self._labels = labels - self._group_index = level_index - self.grouper = level_index.take(labels) - - # Single level index passed - else: - # Use single level index as grouper if none passed - if grouper is None: - self.grouper = index - + self.grouper, self._labels, self._group_index = index._get_grouper_for_level(self.grouper, level) else: if isinstance(self.grouper, (list, tuple)): self.grouper = com._asarray_tuplesafe(self.grouper) diff --git a/pandas/indexes/base.py b/pandas/indexes/base.py index 5082fc84982c6..370bf73239749 100644 --- a/pandas/indexes/base.py +++ b/pandas/indexes/base.py @@ -432,6 +432,13 @@ def _update_inplace(self, result, **kwargs): # guard when called from IndexOpsMixin raise TypeError("Index can't be updated inplace") + def _get_grouper_for_level(self, grouper, level): + # return grouper if grouper is not None else self + if grouper is None: + grouper = self + + return grouper, None, None + def is_(self, other): """ More flexible, faster check like ``is`` but that works through views diff --git a/pandas/indexes/multi.py b/pandas/indexes/multi.py index 0c465da24a17e..e75428361fba8 100644 --- a/pandas/indexes/multi.py +++ b/pandas/indexes/multi.py @@ -539,6 +539,39 @@ def _format_native_types(self, na_rep='nan', **kwargs): return mi.values + def _get_grouper_for_level(self, grouper, level): + + inds = self.labels[level] + level_index = self.levels[level] + + # XXX complete hack + + if grouper is not None: + level_values = self.levels[level].take(inds) + grouper = level_values.map(grouper) + labels = None + level_index = None + else: + # all levels may not be observed + labels, uniques = algos.factorize(inds, sort=True) + + if len(uniques) > 0 and uniques[0] == -1: + # handle NAs + mask = inds != -1 + ok_labels, uniques = algos.factorize(inds[mask], + sort=True) + + labels = np.empty(len(inds), dtype=inds.dtype) + labels[mask] = ok_labels + labels[~mask] = -1 + + if len(uniques) < len(level_index): + level_index = level_index.take(uniques) + + grouper = level_index.take(labels) + + return grouper, labels, level_index + @property def _constructor(self): return MultiIndex.from_tuples From 2b173ab04ba8034bf1a36859881653e4a90bba53 Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Wed, 5 Oct 2016 19:38:55 -0400 Subject: [PATCH 06/13] Improve comments for Index._get_grouper_for_level --- pandas/indexes/base.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pandas/indexes/base.py b/pandas/indexes/base.py index 370bf73239749..38420d8fac620 100644 --- a/pandas/indexes/base.py +++ b/pandas/indexes/base.py @@ -433,10 +433,14 @@ def _update_inplace(self, result, **kwargs): raise TypeError("Index can't be updated inplace") def _get_grouper_for_level(self, grouper, level): - # return grouper if grouper is not None else self + # Use self (Index) as grouper if None was passed if grouper is None: grouper = self + # Return tuple of (grouper, labels, level_index) + # where labels and level_index are None for the Index + # implementation. The labels and level_index values + # are only calculated in the MultiIndex implementation return grouper, None, None def is_(self, other): From f7b1f621b3e58f7b6a752962018b9e9b51127776 Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Wed, 5 Oct 2016 19:43:23 -0400 Subject: [PATCH 07/13] Wrap line violating PEP8 --- pandas/core/groupby.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pandas/core/groupby.py b/pandas/core/groupby.py index cc9c89702a0f1..5223c0ac270f3 100644 --- a/pandas/core/groupby.py +++ b/pandas/core/groupby.py @@ -2204,7 +2204,9 @@ def __init__(self, index, grouper=None, obj=None, name=None, level=None, if self.name is None: self.name = index.names[level] - self.grouper, self._labels, self._group_index = index._get_grouper_for_level(self.grouper, level) + self.grouper, self._labels, self._group_index = \ + index._get_grouper_for_level(self.grouper, level) + else: if isinstance(self.grouper, (list, tuple)): self.grouper = com._asarray_tuplesafe(self.grouper) From bffcbe5375b4d4ace02bcefbf918e8e6ed4be8a8 Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Thu, 6 Oct 2016 07:44:57 -0400 Subject: [PATCH 08/13] Added test cases that group by column then index --- pandas/tests/test_groupby.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pandas/tests/test_groupby.py b/pandas/tests/test_groupby.py index 3163bf61de625..f3791ee1d5c91 100644 --- a/pandas/tests/test_groupby.py +++ b/pandas/tests/test_groupby.py @@ -474,10 +474,21 @@ def test_grouper_column_and_index(self): expected = df_multi.reset_index().groupby(['B', 'inner']).mean() assert_frame_equal(result, expected) + # Test the reverse grouping order + result = df_multi.groupby([pd.Grouper(level='inner'), 'B']).mean() + expected = df_multi.reset_index().groupby(['inner', 'B']).mean() + assert_frame_equal(result, expected) + # Grouping a single-index frame by a column and the index should # be equivalent to resetting the index and grouping by two columns df_single = df_multi.reset_index('outer') result = df_single.groupby(['B', pd.Grouper(level='inner')]).mean() + expected = df_single.reset_index().groupby(['B', 'inner']).mean() + assert_frame_equal(result, expected) + + # Test the reverse grouping order + result = df_single.groupby([pd.Grouper(level='inner'), 'B']).mean() + expected = df_single.reset_index().groupby(['inner', 'B']).mean() assert_frame_equal(result, expected) def test_grouper_getting_correct_binner(self): From f7e0d00aa0409cbd573e7aa522442d712022547d Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Thu, 6 Oct 2016 14:37:35 -0400 Subject: [PATCH 09/13] Cleaned up _get_grouper_for_level implementations and added docstrings --- pandas/indexes/base.py | 32 ++++++++++++++++++----- pandas/indexes/multi.py | 58 ++++++++++++++++++++++++++--------------- 2 files changed, 62 insertions(+), 28 deletions(-) diff --git a/pandas/indexes/base.py b/pandas/indexes/base.py index 38420d8fac620..5f186144297d9 100644 --- a/pandas/indexes/base.py +++ b/pandas/indexes/base.py @@ -432,15 +432,33 @@ def _update_inplace(self, result, **kwargs): # guard when called from IndexOpsMixin raise TypeError("Index can't be updated inplace") - def _get_grouper_for_level(self, grouper, level): - # Use self (Index) as grouper if None was passed - if grouper is None: + def _get_grouper_for_level(self, group_mapper, level): + """ + Get index grouper corresponding to an index level + + Parameters + ---------- + group_mapper: Group mapping function or None + Function mapping index values to groups + level : int + Index level (Only used by MultiIndex override) + + Returns + ------- + grouper : Index + Index of values to group on + labels : None + Array of locations in level_index + (Only returned by MultiIndex override) + level_index : None + Index of unique values for level + (Only returned by MultiIndex override) + """ + if group_mapper is None: grouper = self + else: + grouper = self.map(group_mapper) - # Return tuple of (grouper, labels, level_index) - # where labels and level_index are None for the Index - # implementation. The labels and level_index values - # are only calculated in the MultiIndex implementation return grouper, None, None def is_(self, other): diff --git a/pandas/indexes/multi.py b/pandas/indexes/multi.py index e75428361fba8..426cf4e4062dc 100644 --- a/pandas/indexes/multi.py +++ b/pandas/indexes/multi.py @@ -539,36 +539,52 @@ def _format_native_types(self, na_rep='nan', **kwargs): return mi.values - def _get_grouper_for_level(self, grouper, level): + def _get_grouper_for_level(self, group_mapper, level): + """ + Get index grouper corresponding to an index level + Parameters + ---------- + group_mapper: Group mapping function or None + Function mapping index values to groups + level : int + Index level + + Returns + ------- + grouper : Index + Index of values to group on + labels : ndarray of int or None + Array of locations in level_index + level_index : Index or None + Index of unique values for level + """ inds = self.labels[level] level_index = self.levels[level] - # XXX complete hack - - if grouper is not None: + if group_mapper is not None: + # Handle group mapping function and return level_values = self.levels[level].take(inds) - grouper = level_values.map(grouper) - labels = None - level_index = None - else: - # all levels may not be observed - labels, uniques = algos.factorize(inds, sort=True) + grouper = level_values.map(group_mapper) + return grouper, None, None + + labels, uniques = algos.factorize(inds, sort=True) - if len(uniques) > 0 and uniques[0] == -1: - # handle NAs - mask = inds != -1 - ok_labels, uniques = algos.factorize(inds[mask], - sort=True) + if len(uniques) > 0 and uniques[0] == -1: + # Handle NAs + mask = inds != -1 + ok_labels, uniques = algos.factorize(inds[mask], + sort=True) - labels = np.empty(len(inds), dtype=inds.dtype) - labels[mask] = ok_labels - labels[~mask] = -1 + labels = np.empty(len(inds), dtype=inds.dtype) + labels[mask] = ok_labels + labels[~mask] = -1 - if len(uniques) < len(level_index): - level_index = level_index.take(uniques) + if len(uniques) < len(level_index): + # Remove unobserved levels from level_index + level_index = level_index.take(uniques) - grouper = level_index.take(labels) + grouper = level_index.take(labels) return grouper, labels, level_index From bbe9482d14199c2cece6c077731f8d3df78ac47d Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Thu, 13 Oct 2016 19:36:34 -0400 Subject: [PATCH 10/13] v0.19.1 release notes for fix to GH 14327 --- doc/source/whatsnew/v0.19.1.txt | 1 + doc/source/whatsnew/v0.20.0.txt | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v0.19.1.txt b/doc/source/whatsnew/v0.19.1.txt index 5c03408cbf20f..fc639ef68fc53 100644 --- a/doc/source/whatsnew/v0.19.1.txt +++ b/doc/source/whatsnew/v0.19.1.txt @@ -46,3 +46,4 @@ Bug Fixes - Bug in ``pd.concat`` where names of the ``keys`` were not propagated to the resulting ``MultiIndex`` (:issue:`14252`) - Bug in ``MultiIndex.set_levels`` where illegal level values were still set after raising an error (:issue:`13754`) - Bug in ``DataFrame.to_json`` where ``lines=True`` and a value contained a ``}`` character (:issue:`14391`) +- Bug in ``df.groupby`` causing an ``AttributeError`` when grouping a single index frame by a column and the index (:issue`14327`) diff --git a/doc/source/whatsnew/v0.20.0.txt b/doc/source/whatsnew/v0.20.0.txt index a556d8707a21d..0354a8046e873 100644 --- a/doc/source/whatsnew/v0.20.0.txt +++ b/doc/source/whatsnew/v0.20.0.txt @@ -79,4 +79,3 @@ Performance Improvements Bug Fixes ~~~~~~~~~ -- Bug in ``df.groupby`` causing an ``AttributeError`` when grouping a single index frame by a column and the index (:issue`14327`) From c98155a10fb9ab91ccfcc720fc439b87ceac0b7e Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Fri, 14 Oct 2016 19:34:16 -0400 Subject: [PATCH 11/13] Documentation and naming updates per review (GH 14327) --- pandas/indexes/base.py | 15 +++++++-------- pandas/indexes/multi.py | 22 +++++++++++----------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/pandas/indexes/base.py b/pandas/indexes/base.py index 5f186144297d9..27f03705e7bc7 100644 --- a/pandas/indexes/base.py +++ b/pandas/indexes/base.py @@ -432,16 +432,16 @@ def _update_inplace(self, result, **kwargs): # guard when called from IndexOpsMixin raise TypeError("Index can't be updated inplace") - def _get_grouper_for_level(self, group_mapper, level): + def _get_grouper_for_level(self, mapper, level=None): """ Get index grouper corresponding to an index level Parameters ---------- - group_mapper: Group mapping function or None + mapper: Group mapping function or None Function mapping index values to groups - level : int - Index level (Only used by MultiIndex override) + level : int, default None + Index level Returns ------- @@ -449,15 +449,14 @@ def _get_grouper_for_level(self, group_mapper, level): Index of values to group on labels : None Array of locations in level_index - (Only returned by MultiIndex override) level_index : None Index of unique values for level - (Only returned by MultiIndex override) """ - if group_mapper is None: + assert level is None or level == 0 + if mapper is None: grouper = self else: - grouper = self.map(group_mapper) + grouper = self.map(mapper) return grouper, None, None diff --git a/pandas/indexes/multi.py b/pandas/indexes/multi.py index 426cf4e4062dc..95fa06ffdc777 100644 --- a/pandas/indexes/multi.py +++ b/pandas/indexes/multi.py @@ -539,13 +539,13 @@ def _format_native_types(self, na_rep='nan', **kwargs): return mi.values - def _get_grouper_for_level(self, group_mapper, level): + def _get_grouper_for_level(self, mapper, level): """ Get index grouper corresponding to an index level Parameters ---------- - group_mapper: Group mapping function or None + mapper: Group mapping function or None Function mapping index values to groups level : int Index level @@ -556,27 +556,27 @@ def _get_grouper_for_level(self, group_mapper, level): Index of values to group on labels : ndarray of int or None Array of locations in level_index - level_index : Index or None + uniques : Index or None Index of unique values for level """ - inds = self.labels[level] + indexer = self.labels[level] level_index = self.levels[level] - if group_mapper is not None: + if mapper is not None: # Handle group mapping function and return - level_values = self.levels[level].take(inds) - grouper = level_values.map(group_mapper) + level_values = self.levels[level].take(indexer) + grouper = level_values.map(mapper) return grouper, None, None - labels, uniques = algos.factorize(inds, sort=True) + labels, uniques = algos.factorize(indexer, sort=True) if len(uniques) > 0 and uniques[0] == -1: # Handle NAs - mask = inds != -1 - ok_labels, uniques = algos.factorize(inds[mask], + mask = indexer != -1 + ok_labels, uniques = algos.factorize(indexer[mask], sort=True) - labels = np.empty(len(inds), dtype=inds.dtype) + labels = np.empty(len(indexer), dtype=indexer.dtype) labels[mask] = ok_labels labels[~mask] = -1 From ab05b34c641d5daa9a4a35b8a03a460ef88ed4f3 Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Sat, 15 Oct 2016 12:12:13 -0400 Subject: [PATCH 12/13] Correction to whatsnew. Reference index level instead of index (GH 14327) --- doc/source/whatsnew/v0.19.1.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v0.19.1.txt b/doc/source/whatsnew/v0.19.1.txt index fc639ef68fc53..0046e2553ad6b 100644 --- a/doc/source/whatsnew/v0.19.1.txt +++ b/doc/source/whatsnew/v0.19.1.txt @@ -46,4 +46,4 @@ Bug Fixes - Bug in ``pd.concat`` where names of the ``keys`` were not propagated to the resulting ``MultiIndex`` (:issue:`14252`) - Bug in ``MultiIndex.set_levels`` where illegal level values were still set after raising an error (:issue:`13754`) - Bug in ``DataFrame.to_json`` where ``lines=True`` and a value contained a ``}`` character (:issue:`14391`) -- Bug in ``df.groupby`` causing an ``AttributeError`` when grouping a single index frame by a column and the index (:issue`14327`) +- Bug in ``df.groupby`` causing an ``AttributeError`` when grouping a single index frame by a column and the index level (:issue`14327`) From 9694755e1dea4be8dbcbb3484abd22fdbb90c4a7 Mon Sep 17 00:00:00 2001 From: "Jon M. Mease" Date: Sat, 15 Oct 2016 12:18:19 -0400 Subject: [PATCH 13/13] Convert _get_grouper_for_level doc-strings on Index/MultiIndex to shared_doc (GH 14327) --- pandas/indexes/base.py | 12 +++++++----- pandas/indexes/multi.py | 20 +------------------- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/pandas/indexes/base.py b/pandas/indexes/base.py index 27f03705e7bc7..1c24a0db34b2b 100644 --- a/pandas/indexes/base.py +++ b/pandas/indexes/base.py @@ -432,26 +432,28 @@ def _update_inplace(self, result, **kwargs): # guard when called from IndexOpsMixin raise TypeError("Index can't be updated inplace") - def _get_grouper_for_level(self, mapper, level=None): - """ + _index_shared_docs['_get_grouper_for_level'] = """ Get index grouper corresponding to an index level Parameters ---------- mapper: Group mapping function or None Function mapping index values to groups - level : int, default None + level : int or None Index level Returns ------- grouper : Index Index of values to group on - labels : None + labels : ndarray of int or None Array of locations in level_index - level_index : None + uniques : Index or None Index of unique values for level """ + + @Appender(_index_shared_docs['_get_grouper_for_level']) + def _get_grouper_for_level(self, mapper, level=None): assert level is None or level == 0 if mapper is None: grouper = self diff --git a/pandas/indexes/multi.py b/pandas/indexes/multi.py index 95fa06ffdc777..a9f452db69659 100644 --- a/pandas/indexes/multi.py +++ b/pandas/indexes/multi.py @@ -539,26 +539,8 @@ def _format_native_types(self, na_rep='nan', **kwargs): return mi.values + @Appender(_index_shared_docs['_get_grouper_for_level']) def _get_grouper_for_level(self, mapper, level): - """ - Get index grouper corresponding to an index level - - Parameters - ---------- - mapper: Group mapping function or None - Function mapping index values to groups - level : int - Index level - - Returns - ------- - grouper : Index - Index of values to group on - labels : ndarray of int or None - Array of locations in level_index - uniques : Index or None - Index of unique values for level - """ indexer = self.labels[level] level_index = self.levels[level]