Панды получают лучшие n записей в каждой группе
Предположим, у меня есть DataFrame панд, как это:
>>> df = pd.DataFrame({'id':[1,1,1,2,2,2,2,3,4],'value':[1,2,3,1,2,3,4,1,1]})
>>> df
id value
0 1 1
1 1 2
2 1 3
3 2 1
4 2 2
5 2 3
6 2 4
7 3 1
8 4 1
Я хочу получить новый DataFrame с двумя верхними записями для каждого идентификатора, например:
id value
0 1 1
1 1 2
3 2 1
4 2 2
7 3 1
8 4 1
Я могу сделать это с нумерацией записей в группе за группой:
>>> dfN = df.groupby('id').apply(lambda x:x['value'].reset_index()).reset_index()
>>> dfN
id level_1 index value
0 1 0 0 1
1 1 1 1 2
2 1 2 2 3
3 2 0 3 1
4 2 1 4 2
5 2 2 5 3
6 2 3 6 4
7 3 0 7 1
8 4 0 8 1
>>> dfN[dfN['level_1'] <= 1][['id', 'value']]
id value
0 1 1
1 1 2
3 2 1
4 2 2
7 3 1
8 4 1
Но есть ли более эффективный / элегантный подход для этого? А также есть более элегантный подход к записи чисел в каждой группе (например, функция окна SQL row_number ()).
6 ответов
Ты пробовал df.groupby('id').head(2)
Ouput генерируется:
>>> df.groupby('id').head(2)
id value
id
1 0 1 1
1 1 2
2 3 2 1
4 2 2
3 7 3 1
4 8 4 1
(Имейте в виду, что вам может потребоваться заказать / отсортировать до, в зависимости от ваших данных)
РЕДАКТИРОВАТЬ: Как упоминал спрашивающий, используйте df.groupby('id').head(2).reset_index(drop=True)
убрать мультииндекс и сгладить результаты.
>>> df.groupby('id').head(2).reset_index(drop=True)
id value
0 1 1
1 1 2
2 2 1
3 2 2
4 3 1
5 4 1
Начиная с 0.14.1, теперь вы можете сделать nlargest
а также nsmallest
на groupby
объект:
In [23]: df.groupby('id')['value'].nlargest(2)
Out[23]:
id
1 2 3
1 2
2 6 4
5 3
3 7 1
4 8 1
dtype: int64
Есть небольшая странность, что вы также получаете исходный индекс, но это может быть очень полезно в зависимости от того, каким был ваш исходный индекс.
Если вы не заинтересованы в этом, вы можете сделать .reset_index(level=1, drop=True)
чтобы избавиться от этого вообще.
(Примечание: начиная с версии 0.17.1 вы сможете сделать это и для DataFrameGroupBy, но пока он работает только с Series
а также SeriesGroupBy
.)
Иногда предварительная сортировка всех данных занимает очень много времени. Сначала мы можем группироваться и делать топк для каждой группы:
g = df.groupby(['id']).apply(lambda x: x.nlargest(topk,['value'])).reset_index(drop=True)
Чтобы получить первые N строк каждой группы, можно использовать другой способ:groupby().nth[:N]
. Результат этого вызова такой же, какgroupby().head(N)
. Например, для первых двух строк для каждого идентификатора вызовите:
N = 2
df1 = df.groupby('id', as_index=False).nth[:N]
Чтобы получить наибольшее значение N каждой группы, я предлагаю два подхода.
Сначала отсортируйте по «id» и «value» (не забудьте отсортировать «id» в порядке возрастания, а «value» в порядке убывания, используя
ascending
параметр соответствующим образом), а затем вызовитеgroupby().nth[]
.N = 2 df1 = df.sort_values(by=['id', 'value'], ascending=[True, False]) df1 = df1.groupby('id', as_index=False).nth[:N]
Другой подход заключается в ранжировании значений каждой группы и фильтрации с использованием этих рангов.
# for the entire rows N = 2 msk = df.groupby('id')['value'].rank(method='first', ascending=False) <= N df1 = df[msk] # for specific column rows df1 = df.loc[msk, 'value']
Оба они намного быстрее, чемgroupby().apply()
иgroupby().nlargest()
звонки, как предложено в других ответах здесь ( 1 , 2, 3 ). В образце из 100 тыс. строк и 8000 групп%timeit
тест показал, что это было в 24-150 раз быстрее, чем эти решения.
Кроме того, вместо нарезки вы также можете передать список/кортеж/диапазон в.nth()
вызов:
df.groupby('id', as_index=False).nth([0,1])
# doesn't even have to be consecutive
# the following returns 1st and 3rd row of each id
df.groupby('id', as_index=False).nth([0,2])
df.groupby('id').apply(lambda x : x.sort_values(by = 'value', ascending = False).head(2).reset_index(drop = True))
- Здесь значения сортировки по возрастанию false дают аналогичные nlargest, а True - аналогичные nsmallest.
- Значение внутри заголовка совпадает со значением, которое мы даем внутри nlargest, чтобы получить количество значений, отображаемых для каждой группы.
- reset_index является необязательным и необязательным.
Это работает для повторяющихся значений
Если у вас есть повторяющиеся значения в первых n значениях и вам нужны только уникальные значения, вы можете сделать это следующим образом:
import pandas as pd
ifile = "https://raw.githubusercontent.com/bhishanpdl/Shared/master/data/twitter_employee.tsv"
df = pd.read_csv(ifile,delimiter='\t')
print(df.query("department == 'Audit'")[['id','first_name','last_name','department','salary']])
id first_name last_name department salary
24 12 Shandler Bing Audit 110000
25 14 Jason Tom Audit 100000
26 16 Celine Anston Audit 100000
27 15 Michale Jackson Audit 70000
If we do not remove duplicates, for the audit department we get top 3 salaries as 110k,100k and 100k.
If we want to have not-duplicated salaries per each department, we can do this:
(df.groupby('department')['salary']
.apply(lambda ser: ser.drop_duplicates().nlargest(3))
.droplevel(level=1)
.sort_index()
.reset_index()
)
This gives
department salary
0 Audit 110000
1 Audit 100000
2 Audit 70000
3 Management 250000
4 Management 200000
5 Management 150000
6 Sales 220000
7 Sales 200000
8 Sales 150000