选择每组的最大行 - pandas 性能问题

2024-02-15

我正在为每组选择一个最大行,并且我正在使用groupby/agg返回索引值并使用选择行loc.

例如,分组依据"Id"然后选择最高的行"delta" value:

selected_idx = df.groupby("Id").apply(lambda df: df.delta.argmax())
selected_rows = df.loc[selected_idx, :]

然而,这样的速度实在是太慢了。实际上,当我在 1300 万行上使用此查询时,我的 i7/16G RAM 笔记本电脑挂起。

我有两个问题请教专家:

  1. 如何让这个查询在 pandas 中快速运行?我究竟做错了什么?
  2. 为什么这个手术这么贵?

[更新] 非常感谢@unutbu 的分析!sort_drop这是!在我的 i7/32GRAM 机器上,groupby+idxmax 挂起近 14 小时(从不返回任何东西)sort_drop不到一分钟就处理好了!

我仍然需要看看 pandas 如何实现每个方法,但问题现在已经解决了!我喜欢 StackOverflow。


最快的选项不仅取决于 DataFrame 的长度(在本例中约为 13M 行),还取决于组的数量。下面的性能图比较了寻找每组最大值的多种方法:

If there an only a few (large) groups, using_idxmax may be the fastest option: enter image description here

If there are many (small) groups and the DataFrame is not too large, using_sort_drop may be the fastest option: enter image description here

但请记住,虽然using_sort_drop, using_sort and using_rank开始看起来很快,因为N = len(df)增加,它们相对于其他选项的速度很快消失。对于足够大的N, using_idxmax成为最快的选择,即使有很多组。

using_sort_drop, using_sort and using_rank对 DataFrame(或 DataFrame 中的组)进行排序。排序是O(N * log(N))平均而言,而其他方法使用O(N)运营。这就是为什么像这样的方法using_idxmax beats using_sort_drop对于非常大的数据框。

请注意,基准测试结果可能会因多种原因而有所不同,包括机器规格、操作系统和软件版本。因此,在您自己的机器上运行基准测试并使用适合您情况的测试数据非常重要。

基于上面的性能图,using_sort_drop may be对于 13M 行的 DataFrame,这是一个值得考虑的选项,特别是如果它有许多(小)组。不然我会怀疑using_idxmax成为最快的选择——但同样,检查机器上的基准测试也很重要。


这是我用来制作的设置性能图 https://github.com/nschloe/perfplot:

import numpy as np
import pandas as pd 
import perfplot

def make_df(N):
    # lots of small groups
    df = pd.DataFrame(np.random.randint(N//10+1, size=(N, 2)), columns=['Id','delta'])
    # few large groups
    # df = pd.DataFrame(np.random.randint(10, size=(N, 2)), columns=['Id','delta'])
    return df


def using_idxmax(df):
    return df.loc[df.groupby("Id")['delta'].idxmax()]

def max_mask(s):
    i = np.asarray(s).argmax()
    result = [False]*len(s)
    result[i] = True
    return result

def using_custom_mask(df):
    mask = df.groupby("Id")['delta'].transform(max_mask)
    return df.loc[mask]

def using_isin(df):
    idx = df.groupby("Id")['delta'].idxmax()
    mask = df.index.isin(idx)
    return df.loc[mask]

def using_sort(df):
    df = df.sort_values(by=['delta'], ascending=False, kind='mergesort')
    return df.groupby('Id', as_index=False).first()

def using_rank(df):
    mask = (df.groupby('Id')['delta'].rank(method='first', ascending=False) == 1)
    return df.loc[mask]

def using_sort_drop(df):
    # Thanks to jezrael
    # https://stackoverflow.com/questions/50381064/select-the-max-row-per-group-pandas-performance-issue/50389889?noredirect=1#comment87795818_50389889
    return df.sort_values(by=['delta'], ascending=False, kind='mergesort').drop_duplicates('Id')

def using_apply(df):
    selected_idx = df.groupby("Id").apply(lambda df: df.delta.argmax())
    return df.loc[selected_idx]

def check(df1, df2):
    df1 = df1.sort_values(by=['Id','delta'], kind='mergesort').reset_index(drop=True)
    df2 = df2.sort_values(by=['Id','delta'], kind='mergesort').reset_index(drop=True)
    return df1.equals(df2)

perfplot.show(
    setup=make_df,
    kernels=[using_idxmax, using_custom_mask, using_isin, using_sort, 
             using_rank, using_apply, using_sort_drop],
    n_range=[2**k for k in range(2, 20)],
    logx=True,
    logy=True,
    xlabel='len(df)',
    repeat=75,
    equality_check=check)

另一种基准测试方法是使用IPython %timeit https://stackoverflow.com/a/29280612/190597:

In [55]:  df = make_df(2**20)

In [56]: %timeit using_sort_drop(df)
1 loop, best of 3: 403 ms per loop

In [57]: %timeit using_rank(df)
1 loop, best of 3: 1.04 s per loop

In [58]: %timeit using_idxmax(df)
1 loop, best of 3: 15.8 s per loop
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

选择每组的最大行 - pandas 性能问题 的相关文章