Я использую pandas (0.22.0, python version 3.6.4) .groupby
с методом .nsmallest
, чтобы найти наименьшие элементы в каждой группе кадра данных.Вот пример кадра данных:
>>> import pandas as pd
>>> df = pd.DataFrame({'a': ['foo', 'foo', 'foo', 'foo',
'bar', 'bar', 'bar', 'bar', 'bar',
'qux', 'qux', 'qux'],
'b': ['baz', 'baz', 'baz', 'bat',
'baz', 'baz', 'bat', 'bat', 'bat',
'baz', 'bat', 'bat'],
'c': [1, 3, 2, 5,
6, 4, 9, 12, 7,
10, 8, 11]})
Я хочу три наименьших значения в столбце 'c' для каждой пары 'a' / 'b'.Выражение, которое я использую для получения n-наименьших значений для каждой группы в столбце 'c', выглядит следующим образом:
>>> (df.groupby(['a', 'b'])['c'].apply(lambda x: x.nsmallest(3))
.reset_index(level=['a', 'b']))
Возвращает следующий кадр данных, как и ожидалось:
a b c
8 bar bat 7
6 bar bat 9
7 bar bat 12
5 bar baz 4
4 bar baz 6
3 foo bat 5
0 foo baz 1
2 foo baz 2
1 foo baz 3
10 qux bat 8
11 qux bat 11
9 qux baz 10
Но странная вещь случается, если фрейм данных сортируется от наименьшего к наибольшему в столбце 'c' первым:
>>> df2 = df.sort_values('c', ascending=True)
>>> (df2.groupby(['a', 'b'])['c'].apply(lambda x: x.nsmallest(3))
.reset_index(level=['a', 'b']))
Это возвращает:
---------------------------------------------------------------------------
KeyError Traceback (most recent call last)
<ipython-input-10-2afabcab898a> in <module>()
1 (df2.groupby(['a', 'b'])['c'].apply(lambda x: x.nsmallest(3))
----> 2 .reset_index(level=['a', 'b']))
3
~\AppData\Local\Continuum\anaconda3\lib\site-packages\pandas\core\series.py in reset_index(self, level, drop, name, inplace)
1048 else:
1049 df = self.to_frame(name)
-> 1050 return df.reset_index(level=level, drop=drop)
1051
1052 def __unicode__(self):
~\AppData\Local\Continuum\anaconda3\lib\site-packages\pandas\core\frame.py in reset_index(self, level, drop, inplace, col_level, col_fill)
3339 if not isinstance(level, (tuple, list)):
3340 level = [level]
-> 3341 level = [self.index._get_level_number(lev) for lev in level]
3342 if isinstance(self.index, MultiIndex):
3343 if len(level) < self.index.nlevels:
~\AppData\Local\Continuum\anaconda3\lib\site-packages\pandas\core\frame.py in <listcomp>(.0)
3339 if not isinstance(level, (tuple, list)):
3340 level = [level]
-> 3341 level = [self.index._get_level_number(lev) for lev in level]
3342 if isinstance(self.index, MultiIndex):
3343 if len(level) < self.index.nlevels:
~\AppData\Local\Continuum\anaconda3\lib\site-packages\pandas\core\indexes\base.py in _get_level_number(self, level)
1618
1619 def _get_level_number(self, level):
-> 1620 self._validate_index_level(level)
1621 return 0
1622
~\AppData\Local\Continuum\anaconda3\lib\site-packages\pandas\core\indexes\base.py in _validate_index_level(self, level)
1615 elif level != self.name:
1616 raise KeyError('Level %s must be same as name (%s)' %
-> 1617 (level, self.name))
1618
1619 def _get_level_number(self, level):
KeyError: 'Level a must be same as name (None)'
Очевидно, что .reset_index
- это проблема, поэтому мы удалим это:
>>> df2.groupby(['a', 'b'])['c'].apply(lambda x: x.nsmallest(3))
И мы вернемся к этой серии:
0 1
2 2
1 3
5 4
3 5
4 6
8 7
10 8
6 9
9 10
11 11
7 12
Name: c, dtype: int64
Удаление reset_index
из первого примера показывает MultiIndex:
>>> df.groupby(['a', 'b'])['c'].apply(lambda x: x.nsmallest(3))
a b
bar bat 8 7
6 9
7 12
baz 5 4
4 6
foo bat 3 5
baz 0 1
2 2
1 3
qux bat 10 8
11 11
baz 9 10
Name: c, dtype: int64
Итак, что-то в отсортированном фрейме данных вызвало выпадение MultiIndex из операции groupby
.То же самое происходит, если мы сортируем от наибольшего к наименьшему и вызываем nlargest
:
>>> df3 = df.sort_values('c', ascending=False)
>>> df3.groupby(['a', 'b'])['c'].apply(lambda x: x.nlargest(3))
7 12
11 11
9 10
6 9
10 8
8 7
4 6
3 5
5 4
1 3
2 2
0 1
Name: c, dtype: int64
И то же самое происходит, даже если мы пытаемся стать хитрым с отрицательным знаком:
>>> df3.groupby(['a', 'b'])['c'].apply(lambda x: (-x).nsmallest(3))
7 -12
11 -11
9 -10
6 -9
10 -8
8 -7
4 -6
3 -5
5 -4
1 -3
2 -2
0 -1
Name: c, dtype: int64
Но не в том случае, если мы используем nlargest
с отрицательным знаком:
>>> df3.groupby(['a', 'b'])['c'].apply(lambda x: (-x).nlargest(3))
a b
bar bat 8 -7
6 -9
7 -12
baz 5 -4
4 -6
foo bat 3 -5
baz 0 -1
2 -2
1 -3
qux bat 10 -8
11 -11
baz 9 -10
Name: c, dtype: int64
Я много играл с этим, и я довольно озадачен.Вы можете спросить «Зачем сортировать фрейм данных, если вы знаете, что это вызовет эту ошибку?», Но это происходит с nsmallest
, если хотя бы одна из групп сортируется по возрастанию, и с nlargest
, если группа сортируется по убыванию,Вот краткий пример:
>>> df4 = pd.DataFrame({'a': ['foo', 'foo', 'foo', 'bar', 'bar'],
'b': ['baz', 'baz', 'bat', 'baz', 'bat'],
'c': [1, 2, 10, 4, 7]})
a b c
0 foo baz 1
1 foo baz 2
2 foo bat 10
3 bar baz 4
4 bar bat 7
>>> df4.groupby(['a', 'b'])['c'].apply(lambda x: x.nsmallest(3))
0 1
1 2
2 10
3 4
4 7
Name: c, dtype: int64
Ожидается ли такое поведение или это ошибка в пандах?Кто-нибудь может порекомендовать решение этой ошибки?Прямо сейчас я просто оборонительно сортирую фрейм данных в противоположном направлении перед использованием groupby
и nsmallest
:
>>> df5 = df4.sort_values('c', ascending=False)
>>> (df5.groupby(['a', 'b'])['c'].apply(lambda x: x.nsmallest(3))
.reset_index(level=['a', 'b']))
a b c
4 bar bat 7
3 bar baz 4
2 foo bat 10
0 foo baz 1
1 foo baz 2
Но это кажется ненужным и грязным.Будем благодарны за любые идеи или идеи!
Редактировать 18.06.18: Просматривая ссылки, предложенные @gyoza, я понимаю, что проблема не в nsmallest
или nlargest
скорее с результатами операции apply
над объектом groupby.Если ряд, возвращаемый операцией apply
, имеет тот же индекс, что и исходная группа groupby, pandas возвращает исходный индекс, а не multiIndex.
Решение @ gyoza создает серию в операции apply с новым индексомчтобы гарантировать, что multiIndex возвращается.В моем реальном коде, однако, более поздний шаг (который помечает наименьшее в каждой группе для просмотра) зависит от исходного индекса, сохраняемого с помощью операции apply.Я мог бы переписать этот шаг как объединение столбцов группировки вместо индексации с .loc
, но я бы предпочел этого не делать.