执行 pandas groupby 操作的更快替代方案

Posted

技术标签:

【中文标题】执行 pandas groupby 操作的更快替代方案【英文标题】:Faster alternative to perform pandas groupby operation 【发布时间】:2019-01-29 05:25:19 【问题描述】:

我有一个以名称 (person_name)、日期和颜色 (shirt_color) 为列的数据集。

每个人在特定的日子都穿着某种颜色的衬衫。天数可以是任意的。

例如输入:

name  day  color
----------------
John   1   White
John   2   White
John   3   Blue
John   4   Blue
John   5   White
Tom    2   White
Tom    3   Blue
Tom    4   Blue
Tom    5   Black
Jerry  1   Black
Jerry  2   Black
Jerry  4   Black
Jerry  5   White

我需要找到每个人最常用的颜色。

例如结果:

name    color
-------------
Jerry   Black
John    White
Tom     Blue

我正在执行以下操作以获得结果,效果很好,但速度很慢:

most_frquent_list = [[name, group.color.mode()[0]] 
                        for name, group in data.groupby('name')]
most_frquent_df = pd.DataFrame(most_frquent_list, columns=['name', 'color'])

现在假设我有一个包含 500 万个唯一名称的数据集。执行上述操作的最佳/最快方法是什么?

【问题讨论】:

我希望有人会对所有这些提交的内容进行基准测试。我现在就做,但已经晚了。 @AndréC.Andersen 我将为每个解决方案添加基准测试 cmets。 每个循环 1.91 毫秒 ± 2.35 微秒(平均值 ± 标准偏差,7 次运行,每次 1000 次循环) 【参考方案1】:

Numpy 的 numpy.add.atpandas.factorize

这是为了快速。但是,我尝试将其组织为可读性。

i, r = pd.factorize(df.name)
j, c = pd.factorize(df.color)
n, m = len(r), len(c)

b = np.zeros((n, m), dtype=np.int64)

np.add.at(b, (i, j), 1)
pd.Series(c[b.argmax(1)], r)

John     White
Tom       Blue
Jerry    Black
dtype: object

groupbysizeidxmax

df.groupby(['name', 'color']).size().unstack().idxmax(1)

name
Jerry    Black
John     White
Tom       Blue
dtype: object

name
Jerry    Black
John     White
Tom       Blue
Name: color, dtype: object

Counter

¯\_(ツ)_/¯

from collections import Counter

df.groupby('name').color.apply(lambda c: Counter(c).most_common(1)[0][0])

name
Jerry    Black
John     White
Tom       Blue
Name: color, dtype: object

【讨论】:

第一次:每个循环 362 µs ± 1.47 µs(平均值 ± 标准偏差,7 次运行,每次 1000 次循环) 第二次:每个循环 1.51 毫秒 ± 4.67 微秒(平均值 ± 标准偏差,7 次运行,每次 1000 次循环) 第 3 次:每个循环 834 µs ± 2.66 µs(平均值 ± 标准偏差,7 次运行,每次 1000 次循环)【参考方案2】:

更新

这一定很难击败(示例 daraframe 上的速度比任何建议的 pandas 解决方案快约 10 倍,比建议的 numpy 解决方案快 1.5 倍)。要点是远离 pandas 并使用 itertools.groupby 在涉及非数字数据时做得更好。

from itertools import groupby
from collections import Counter

pd.Series(x: Counter(z[-1] for z in y).most_common(1)[0][0] for x,y 
          in groupby(sorted(df.values.tolist()), 
                            key=lambda x: x[0]))
# Jerry    Black
# John     White
# Tom       Blue

旧答案

这是另一种方法。它实际上比原来的慢,但我会保留在这里:

data.groupby('name')['color']\
    .apply(pd.Series.value_counts)\
    .unstack().idxmax(axis=1)
# name
# Jerry    Black
# John     White
# Tom       Blue

【讨论】:

哈!我就是这么做的。我会删除 @piRSquared 来吧,坚持下去!让 OP 决定。 @piRSquared Yours Counter 由于apply,仍然处于较慢的一侧。这里的重点是不要搞砸熊猫。 我认为collections 解决方案很好,但将其称为比 pandas/numpy 快 10 倍的做法具有误导性。在只有几百行的数据帧上,piRSquared 的因式分解解决方案轻松击败它,并且样本数据帧上的时间永远不会意味着太多 @user3483203 同意。我添加了一条注释,即 10 倍加速仅在示例数据帧上可见。【参考方案3】:

来自pd.Series.mode

的解决方案
df.groupby('name').color.apply(pd.Series.mode).reset_index(level=1,drop=True)
Out[281]: 
name
Jerry    Black
John     White
Tom       Blue
Name: color, dtype: object

【讨论】:

对不起,我想念这个问题并修复了。 每个循环 1.66 毫秒 ± 3.48 微秒(平均值 ± 标准偏差,7 次运行,每次 1000 次循环)【参考方案4】:

transform(max)做两个分组怎么样?

df = df.groupby(["name", "color"], as_index=False, sort=False).count()
idx = df.groupby("name", sort=False).transform(max)["day"] == df["day"]
df = df[idx][["name", "color"]].reset_index(drop=True)

输出:

    name  color
0   John  White
1    Tom   Blue
2  Jerry  Black

【讨论】:

每个循环 12.2 ms ± 48.4 µs(平均值 ± 标准偏差,7 次运行,每次 100 个循环) 谢谢。那不是很好,从我看到其他人得到的情况来看。作为时间测试的反馈,如果您在同一个小数据集上循环多次,它可能无法与在大型数据集上循环一次相比。许多像这样的解决方案启动成本很高,但一旦开始处理就会表现良好。多次循环一个小数据集意味着您可能只是在衡量启动成本,这应该只是一次成本。我建议你增加你正在测试的数据集的大小,直到运行一个循环需要几秒钟。【参考方案5】:

类似于@piRSquared 的pd.factorizenp.add.at ans。

我们使用

对列中的字符串进行编码
i, r = pd.factorize(df.name)
j, c = pd.factorize(df.color)
n, m = len(r), len(c)
b = np.zeros((n, m), dtype=np.int64)

但是,不要这样做:

np.add.at(b, (i, j), 1)
max_columns_after_add_at = b.argmax(1)

我们使用 jited 函数得到 max_columns_after_add_at,在同一个循环中添加并找到最大值:

@nb.jit(nopython=True, cache=True)
def add_at(x, rows, cols, val):
    max_vals = np.zeros((x.shape[0], ), np.int64)
    max_inds = np.zeros((x.shape[0], ), np.int64)
    for i in range(len(rows)):
        r = rows[i]
        c = cols[i]
        x[r, c]+=1
        if(x[r, c] > max_vals[r]):
            max_vals[r] = x[r, c]
            max_inds[r] = c
    return max_inds

然后最后得到dataframe,

ans = pd.Series(c[max_columns_after_add_at], r)

所以,区别在于我们如何处理argmax(axis=1) after np.add.at()

时序分析

import numpy as np
import numba as nb
m = 100000
n = 100000
rows = np.random.randint(low = 0, high = m, size=10000)
cols = np.random.randint(low = 0, high = n, size=10000)

所以这个:

%%time
x = np.zeros((m,n))
np.add.at(x, (rows, cols), 1)
maxs = x.argmax(1)

给予:

CPU 时间:用户 12.4 秒,系统:38 秒,总计:50.4 秒挂壁时间:50.5 秒

还有这个

%%time
x = np.zeros((m,n))
maxs2 = add_at(x, rows, cols, 1)

给予

CPU 时间:用户 108 毫秒,系统:39.4 秒,总计:39.5 秒挂壁时间:38.4 秒

【讨论】:

【参考方案6】:

由于使用很小的测试 DataFrame 作为输入进行测量,其他答案中讨论的大多数测试结果都存在偏差。 Pandas 有一些固定但通常可以忽略不计的设置时间,但在处理这个微小的数据集时会显得很重要。

在更大的数据集上,最快的方法是使用pd.Series.mode()agg()

df.groupby('name')['color'].agg(pd.Series.mode)

测试台:

arr = np.array([
    ('John',   1,   'White'),
    ('John',   2,  'White'),
    ('John',   3,   'Blue'),
    ('John',   4,   'Blue'),
    ('John',   5,   'White'),
    ('Tom',    2,   'White'),
    ('Tom',    3,   'Blue'),
    ('Tom',    4,   'Blue'),
    ('Tom',    5,   'Black'),
    ('Jerry',  1,   'Black'),
    ('Jerry',  2,   'Black'),
    ('Jerry',  4,   'Black'),
    ('Jerry',  5,   'White')],
    dtype=[('name', 'O'), ('day', 'i8'), ('color', 'O')])

from timeit import Timer
from itertools import groupby
from collections import Counter

df = pd.DataFrame.from_records(arr).sample(100_000, replace=True)

def factorize():
    i, r = pd.factorize(df.name)
    j, c = pd.factorize(df.color)
    n, m = len(r), len(c)

    b = np.zeros((n, m), dtype=np.int64)

    np.add.at(b, (i, j), 1)
    return pd.Series(c[b.argmax(1)], r)

t_factorize = Timer(lambda: factorize())
t_idxmax = Timer(lambda: df.groupby(['name', 'color']).size().unstack().idxmax(1))
t_aggmode = Timer(lambda: df.groupby('name')['color'].agg(pd.Series.mode))
t_applymode = Timer(lambda: df.groupby('name').color.apply(pd.Series.mode).reset_index(level=1,drop=True))
t_aggcounter = Timer(lambda: df.groupby('name')['color'].agg(lambda c: Counter(c).most_common(1)[0][0]))
t_applycounter = Timer(lambda: df.groupby('name').color.apply(lambda c: Counter(c).most_common(1)[0][0]))
t_itertools = Timer(lambda: pd.Series(
    x: Counter(z[-1] for z in y).most_common(1)[0][0] for x,y
      in groupby(sorted(df.values.tolist()), key=lambda x: x[0])))

n = 100
[print(r) for r in (
    f"t_factorize.timeit(number=n)=",
    f"t_idxmax.timeit(number=n)=",
    f"t_aggmode.timeit(number=n)=",
    f"t_applymode.timeit(number=n)=",
    f"t_applycounter.timeit(number=n)=",
    f"t_aggcounter.timeit(number=n)=",
    f"t_itertools.timeit(number=n)=",
)]
t_factorize.timeit(number=n)=1.325189442
t_idxmax.timeit(number=n)=1.0613339019999999
t_aggmode.timeit(number=n)=1.0495010750000002
t_applymode.timeit(number=n)=1.2837302849999999
t_applycounter.timeit(number=n)=1.9432825890000007
t_aggcounter.timeit(number=n)=1.8283823839999993
t_itertools.timeit(number=n)=7.0855046380000015

【讨论】:

【参考方案7】:

想把上表转换成数据框试试贴出来的答案,可以用这个sn-p。将上面的表格复制粘贴到笔记本单元格中,如下所示,确保删除连字符

l = """name  day  color
John   1   White
John   2   White
John   3   Blue
John   4   Blue
John   5   White
Tom    2   White
Tom    3   Blue
Tom    4   Blue
Tom    5   Black
Jerry  1   Black
Jerry  2   Black
Jerry  4   Black
Jerry  5   White""".split('\n')

现在我们需要将此列表转换为元组列表。

df = pd.DataFrame([tuple(i.split()) for i in l])
headers = df.iloc[0]
new_df  = pd.DataFrame(df.values[1:], columns=headers)

现在使用 new_df,你可以参考上面的答案@piRSquared

【讨论】:

以上是关于执行 pandas groupby 操作的更快替代方案的主要内容,如果未能解决你的问题,请参考以下文章

使用 pandas groupby + apply 和 condensing groups 计算平均值的更快方法

Pandas | 18 GroupBy 分组

如何对一列执行 pandas groupby 操作,但将另一列保留在结果数据框中

为啥 pandas.groupby 保留密钥?

pandas groupby apply 真的很慢

从 Pandas groupBy 到 PySpark groupBy