【问题标题】:Why is pandas nlargest slower than mine?为什么 pandas nlargest 比我的慢?
【发布时间】:2019-06-03 02:17:43
【问题描述】:

我有一个数据框

            ID  CAT    SCORE
0            0    0  8325804
1            0    1  1484405
...        ...  ...      ...
1999980  99999    0  4614037
1999981  99999    1  1818470

我将数据按ID 分组,并想知道每个 ID 中得分最高的 2 个类别。我可以看到两种解决方案:

df2 = df.groupby('ID').apply(lambda g: g.nlargest(2, columns='SCORE'))

或手动将其转换为元组列表,对元组进行排序,删除除两个之外的每个 ID,然后转换回数据帧。第一个应该比第二个快得多,但我观察到手动解决方案要快得多。

为什么手动 nlargest 比 pandas 解决方案更快?

MVCE

import numpy as np
import pandas as pd
import time


def create_df(n=10**5, categories=20):
    np.random.seed(0)
    df = pd.DataFrame({'ID': [id_ for id_ in range(n) for c in range(categories)],
                       'CAT': [c for id_ in range(n) for c in range(categories)],
                       'SCORE': np.random.randint(10**7, size=n * categories)})
    return df


def are_dfs_equal(df1, df2):
    columns = sorted(df1.columns)
    if len(df1.columns) != len(df2.columns):
        return False
    elif not all(el1 == el2 for el1, el2 in zip(columns, sorted(df2.columns))):
        return False
    df1_list = [tuple(x) for x in df1[columns].values]
    df1_list = sorted(df1_list, reverse=True)
    df2_list = [tuple(x) for x in df2[columns].values]
    df2_list = sorted(df2_list, reverse=True)
    is_same = df1_list == df2_list
    return is_same


def manual_nlargest(df, n=2):
    df_list = [tuple(x) for x in df[['ID', 'SCORE', 'CAT']].values]
    df_list = sorted(df_list, reverse=True)
    l = []
    current_id = None
    current_id_count = 0
    for el in df_list:
        if el[0] != current_id:
            current_id = el[0]
            current_id_count = 1
        else:
            current_id_count += 1
        if current_id_count <= n:
            l.append(el)
    df = pd.DataFrame(l, columns=['ID', 'SCORE', 'CAT'])
    return df

df = create_df()

t0 = time.time()
df2 = df.groupby('ID').apply(lambda g: g.nlargest(2, columns='SCORE'))
t1 = time.time()
print('nlargest solution: {:0.2f}s'.format(t1 - t0))

t0 = time.time()
df3 = manual_nlargest(df, n=2)
t1 = time.time()
print('manual nlargest solution: {:0.2f}s'.format(t1 - t0))
print('is_same: {}'.format(are_dfs_equal(df2, df3)))

给予

nlargest solution: 97.76s
manual nlargest solution: 4.62s
is_same: True

【问题讨论】:

  • 这比你的 apply() 快,但比手动慢:df_group = df.groupby(['ID'])['SCORE'].nlargest(2)
  • 对于此类基准测试,如果您想要相当可靠的结果,您应该使用timeit 模块。话虽如此,使用 timeit 测试您的代码,我仍然会在两种解决方案之间得到一致且巨大的差异(=~63 vs =~ 5s)。只是一个问题:您是否检查过两种解决方案的结果是否相同?
  • @brunodesthuilliers 我敢肯定它是一样的。请查看更新。
  • @SandervandenOord 是的,它更快。但后来我只得到了 ID / 分数,但失去了 CAT

标签: python pandas dataframe pandas-groupby


【解决方案1】:

这是一个比您的手动解决方案更快的解决方案,除非我犯了错误;)如果您需要速度,我猜 nlargest() 不是解决此问题的最快方法,但它是更具可读性的解决方案。

t0 = time.time()
df4 = df.sort_values(by=['ID', 'SCORE'], ascending=[True, False])
df4['cumcount'] = df4.groupby('ID')['SCORE'].cumcount()
df4 = df4[df4['cumcount'] < 2]
t1 = time.time()
print('cumcount solution: {:0.2f}s'.format(t1 - t0))

【讨论】:

  • 删除“cumcount”列后,数据框看起来像预期的结果。不错的解决方案!
  • 不过,Lamine 的解决方案更快、更简单。
【解决方案2】:

我猜你可以用这个:

df.sort_values(by=['SCORE'],ascending=False).groupby('ID').head(2)

这与您在 pandas groupby 上使用 Sort/head 函数的手动解决方案相同。

t0 = time.time()
df4 = df.sort_values(by=['SCORE'],ascending=False).groupby('ID').head(2)
t1 = time.time()
df4_list = [tuple(x) for x in df4[['ID', 'SCORE', 'CAT']].values]
df4_list = sorted(df4_list, reverse=True)
is_same = df3_list == df4_list
print('SORT/HEAD solution: {:0.2f}s'.format(t1 - t0))
print(is_same)

给予

SORT/HEAD solution: 0.08s
True

时间

77.9 ms ± 7.91 ms per loop (mean ± std. dev. of 7 runs, 10 loops each).

至于为什么nlargest 比其他解决方案慢?我猜为每个组调用它会产生开销(%prun 在 30.293 秒内显示 15764409 个函数调用(15464352 个原始调用)。

对于这个解决方案(1533 个函数调用(1513 个原始调用)在 0.078 秒内)

【讨论】:

  • 您能否解释一下您是如何获得 timeit 值的?
  • @MartinThoma 我使用 jupyter notebooks 上的内置魔法命令 (%timeit) timeit
  • @Mortz 是的,它确实给出了相同的结果,因为 pandas groupby preserves the order of rows within each group
  • @Lamine - 谢谢,我也检查过了。很好的答案。事实上,我错过了你已经在你的答案中验证了它
  • 有趣的是,df.nlargest(n) 在文档中被描述为df.sort_values(columns, ascending=False).head(n) 的“更高性能”版本。 (请参阅:pandas.pydata.org/pandas-docs/stable/generated/…)但这篇文章会提出相反的建议,至少在处理 groupBy 对象时是这样。
猜你喜欢
  • 1970-01-01
  • 2018-07-26
  • 2011-03-23
  • 2017-05-29
  • 2017-03-16
  • 2014-02-09
  • 2018-09-25
相关资源
最近更新 更多