如何加快pandas groupby bins的agg?

Posted

技术标签:

【中文标题】如何加快pandas groupby bins的agg?【英文标题】:How to speed up the agg of pandas groupby bins? 【发布时间】:2022-01-23 20:39:12 【问题描述】:

我为每一列创建了不同的 bin,并根据这些对 DataFrame 进行了分组。

import pandas as pd
import numpy as np

np.random.seed(100)
df = pd.DataFrame(np.random.randn(100, 4), columns=['a', 'b', 'c', 'value'])

# for simplicity, I use the same bin here
bins = np.arange(-3, 4, 0.05)

df['a_bins'] = pd.cut(df['a'], bins=bins)
df['b_bins'] = pd.cut(df['b'], bins=bins)
df['c_bins'] = pd.cut(df['c'], bins=bins)

df.groupby(['a_bins','b_bins','c_bins']).size() 的输出表示组长为2685619。

计算每组的统计数据

然后,每个组的统计数据是这样计算的:

%%timeit
df.groupby(['a_bins','b_bins','c_bins']).agg('value':['mean'])

>>> 16.9 s ± 637 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

预期输出

    是否可以加快速度? 更快的方法还应该支持通过输入a, b, and c 值来查找值,如下所示:
df.groupby(['a_bins','b_bins','c_bins']).agg('value':['mean']).loc[(-1.72, 0.32, 1.18)]

>>> -0.252436

【问题讨论】:

请创建一个具有预期输出的示例数据框,以便我们确定我们的结果匹配并且我们在正确的轨道上 @sammywemmy 感谢您的建议。 np.random.seed() 可以确保我们拥有相同的 DataFrame。我现在更新了预期的输出。 【参考方案1】:

另一种直接的解决方案,基于convtools,它能够处理输入数据流并且不需要输入数据适合内存:

import numpy as np
import pandas as pd

from convtools import conversion as c


def c_bin(left, right, bin_size):
    return c.if_(
        c.or_(c.this < left, c.this > right),
        None,
        ((c.this - left) // bin_size).pipe(
            (c.this * bin_size + left, (c.this + 1) * bin_size + left)
        ),
    )


to_binned = c_bin(-3, 4, 0.05)
to_interval = c.if_(c.this, c.apply_func(pd.Interval, c.this, ), None)

a_bins = c.item(0).pipe(to_binned)
b_bins = c.item(1).pipe(to_binned)
c_bins = c.item(2).pipe(to_binned)
converter = (
    c.group_by(a_bins, b_bins, c_bins)
    .aggregate(
        
            "a_bins": a_bins.pipe(to_interval),
            "b_bins": b_bins.pipe(to_interval),
            "c_bins": c_bins.pipe(to_interval),
            "value_mean": c.ReduceFuncs.Average(c.item(3)),
        
    )
    .gen_converter()
)


np.random.seed(100)
data = np.random.randn(100, 4)

df = pd.DataFrame(converter(data)).set_index(["a_bins", "b_bins", "c_bins"])
df.loc[(-1.72, 0.32, 1.18)]

时间安排:

In [44]: %timeit converter(data)
438 µs ± 1.59 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

# passing back to pandas, timing the end-to-end thing:
In [43]: %timeit pd.DataFrame(converter(data)).set_index(["a_bins", "b_bins", "c_bins"]).loc[(-1.72, 0.32, 1.18)]
2.37 ms ± 14.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

JFYI:converter(data) 的输出缩短:

[
 ...,
 'a_bins': Interval(-0.44999999999999973, -0.3999999999999999, closed='right'),
  'b_bins': Interval(0.7000000000000002, 0.75, closed='right'),
  'c_bins': Interval(-0.19999999999999973, -0.1499999999999999, closed='right'),
  'value_mean': -0.08605564337254189,
 'a_bins': Interval(-0.34999999999999964, -0.2999999999999998, closed='right'),
  'b_bins': Interval(-0.1499999999999999, -0.09999999999999964, closed='right'),
  'c_bins': Interval(0.050000000000000266, 0.10000000000000009, closed='right'),
  'value_mean': 0.18971879197958597,
 'a_bins': Interval(-2.05, -2.0, closed='right'),
  'b_bins': Interval(0.75, 0.8000000000000003, closed='right'),
  'c_bins': Interval(-0.25, -0.19999999999999973, closed='right'),
  'value_mean': -1.1844114274105708]

【讨论】:

感谢这个神奇的工具。它真的很快!是否可以满足Expected output的第二个要求?顺便说一句,你能解释一下为什么这比 @sammywemmy 方法快得多吗? @XinZhang 希望这将成为您工具包中极地/熊猫的一个不错的补充! :) 我已经更新了上面的内容以满足第二个要求(错过了这部分,对此感到抱歉)。关于速度,它远没有 Polars/pandas 快,后者在较低级别上执行并使用矢量化。然而,convtools 是用来生成简单快速的原始 python ad hoc 代码来通过动态代码生成来解决问题的,有时它会有所帮助:) 而且它还可以大大提高代码重用! 很棒的图书馆。进入我的工具包:) @Nikita Almakov 谢谢!太棒了;)【参考方案2】:

这是scipy.stats.binned_statistic_dd 的一个很好的用例。下面的 sn-p 仅计算平均统计量,但支持许多其他统计量(请参阅上面链接的文档):

import numpy as np
import pandas as pd

np.random.seed(100)
df = pd.DataFrame(np.random.randn(100, 4), columns=["a", "b", "c", "value"])

# for simplicity, I use the same bin here
bins = np.arange(-3, 4, 0.05)

df["a_bins"] = pd.cut(df["a"], bins=bins)
df["b_bins"] = pd.cut(df["b"], bins=bins)
df["c_bins"] = pd.cut(df["c"], bins=bins)

# this takes about 35 seconds
result_pandas = df.groupby(["a_bins", "b_bins", "c_bins"]).agg("value": ["mean"])

from scipy.stats import binned_statistic_dd

# this takes about 20 ms
result_scipy = binned_statistic_dd(
    df[["a", "b", "c"]].to_numpy(), df["value"], bins=(bins, bins, bins)
)

# this is a verbose way to get a dataframe representation
# for many purposes this probably will not be needed
# takes about 5 seconds
temp_list = []
for na, a in enumerate(result_scipy[1][0][:-1]):
    for nb, b in enumerate(result_scipy[1][1][:-1]):
        for nc, c in enumerate(result_scipy[1][2][:-1]):
            value = result_scipy[0][na, nb, nc]
            temp_list.append([a, b, c, value])

result_scipy_as_df = pd.DataFrame(temp_list, columns=list("abcx"))

# check that the result is the same
result_scipy_as_df["x"].describe() == result_pandas["value"]["mean"].describe()

如果您有兴趣进一步加快速度,answer 可能会有用。

一个重要的警告是binned_statistic_dd 使用右侧关闭的垃圾箱,例如[0,1),除了最后一个(请参阅链接文档中的注释),因此对于一致的 bin 标识符,必须在 pd.cut 中使用 right=False

这是一个查找示例,请注意这里确切的 bin 边缘位置增加了 1 以获得与 pandas 中相似的结果:

aloc, bloc, cloc = -2.12, 0.23, -1.25
print(result_pandas.loc[(aloc, bloc, cloc)])
print(result_scipy.statistic[
    np.digitize(aloc, result_scipy.bin_edges[0][1:]),
    np.digitize(bloc, result_scipy.bin_edges[1][1:]),
    np.digitize(cloc, result_scipy.bin_edges[2][1:]),
])

【讨论】:

哦,我意识到 @sammywemmy 的方法会降低 NaN 值。如果用户需要 NaN 值,那么您的答案非常有用!非常感谢;) 注意np.digitize()要加上right=True,否则最大值超出索引。【参考方案3】:

对于这些数据,我建议您对数据进行透视,并通过平均值。通常,这会更快,因为您要访问整个数据帧,而不是遍历每个组:

(df
 .pivot(None, ['a_bins', 'b_bins', 'c_bins'], 'value')
 .mean()
 .sort_index() # ignore this if you are not fuzzy on order
)

a_bins         b_bins         c_bins       
(-2.15, -2.1]  (0.25, 0.3]    (-1.3, -1.25]    0.929100
               (0.75, 0.8]    (-0.3, -0.25]    0.480411
(-2.05, -2.0]  (-0.1, -0.05]  (0.3, 0.35]     -1.684900
               (0.75, 0.8]    (-0.25, -0.2]   -1.184411
(-2.0, -1.95]  (-0.6, -0.55]  (-1.2, -1.15]   -0.021176
                                                 ...   
(1.7, 1.75]    (-0.75, -0.7]  (1.05, 1.1]     -0.229518
(1.85, 1.9]    (-0.4, -0.35]  (1.8, 1.85]      0.003017
(1.9, 1.95]    (-1.45, -1.4]  (0.1, 0.15]      0.949361
(2.05, 2.1]    (-0.35, -0.3]  (-0.65, -0.6]    0.763184
(2.25, 2.3]    (-0.95, -0.9]  (0.1, 0.15]      2.539432

这与 groupby 的输出相匹配:

(df
 .groupby(['a_bins','b_bins','c_bins'])
 .agg('value':['mean'])
 .dropna()
 .squeeze()
)

a_bins         b_bins         c_bins       
(-2.15, -2.1]  (0.25, 0.3]    (-1.3, -1.25]    0.929100
               (0.75, 0.8]    (-0.3, -0.25]    0.480411
(-2.05, -2.0]  (-0.1, -0.05]  (0.3, 0.35]     -1.684900
               (0.75, 0.8]    (-0.25, -0.2]   -1.184411
(-2.0, -1.95]  (-0.6, -0.55]  (-1.2, -1.15]   -0.021176
                                                 ...   
(1.7, 1.75]    (-0.75, -0.7]  (1.05, 1.1]     -0.229518
(1.85, 1.9]    (-0.4, -0.35]  (1.8, 1.85]      0.003017
(1.9, 1.95]    (-1.45, -1.4]  (0.1, 0.15]      0.949361
(2.05, 2.1]    (-0.35, -0.3]  (-0.65, -0.6]    0.763184
(2.25, 2.3]    (-0.95, -0.9]  (0.1, 0.15]      2.539432
Name: (value, mean), Length: 100, dtype: float64

pivot 选项在我的 PC 上的速度为 3.72 毫秒,而我不得不终止 groupby 选项,因为它花费的时间太长(我的 PC 很旧:))

同样,这个工作/更快的原因是因为平均值是针对整个数据帧,而不是通过 groupby 中的组。

至于您的其他问题,您可以轻松索引:


bin_mean = (df
 .pivot(None, ['a_bins', 'b_bins', 'c_bins'], 'value')
 .mean()
 .sort_index() # ignore this if you are not fuzzy on order
)

bin_mean.loc[(-1.72, 0.32, 1.18)]
 -0.25243603652138985

主要问题是分类的 Pandas 将返回所有行(这是浪费的,而且效率不高);通过observed = True,您应该会注意到显着的改进:

(df.groupby(['a_bins','b_bins','c_bins'], observed=True)
   .agg('value':['mean'])
)

                                              value
                                               mean
a_bins        b_bins        c_bins                 
(-2.15, -2.1] (0.25, 0.3]   (-1.3, -1.25]  0.929100
              (0.75, 0.8]   (-0.3, -0.25]  0.480411
(-2.05, -2.0] (-0.1, -0.05] (0.3, 0.35]   -1.684900
              (0.75, 0.8]   (-0.25, -0.2] -1.184411
(-2.0, -1.95] (-0.6, -0.55] (-1.2, -1.15] -0.021176
...                                             ...
(1.7, 1.75]   (-0.75, -0.7] (1.05, 1.1]   -0.229518
(1.85, 1.9]   (-0.4, -0.35] (1.8, 1.85]    0.003017
(1.9, 1.95]   (-1.45, -1.4] (0.1, 0.15]    0.949361
(2.05, 2.1]   (-0.35, -0.3] (-0.65, -0.6]  0.763184
(2.25, 2.3]   (-0.95, -0.9] (0.1, 0.15]    2.539432

在我的 PC 上速度约为 7.39 毫秒,比枢轴选项小约 2 倍,但现在速度更快,这是因为仅使用/返回数据框中存在的分类。

【讨论】:

pivot 的好例子!但是,当我将数据增加到 100000 行时,它会提高Unable to allocate 65.2 GiB for an array with shape (100000, 87554) and data type float64。 @Nikita Almakov 方法仍然有效。 嗯……那是很多记忆。当您使用观察 = True 运行 groupby 时,同样的问题? @NikitaAlmakov 的图书馆很棒。 哈,我只测试了pivotobserved=True 效果很好,比@NikitaAlmakov 的方法快! convtools:777 ms ± 30.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each),观察=真:32.1 ms ± 592 µs per loop (mean ± std. dev. of 7 runs, 10 loops each) 注意1000长度的数据,它们是相似的。 observed=True7.06 ms ± 373 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)convtools7.32 ms ± 667 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)。很难选择哪一个是答案,哈哈。也许您可以为不同长度的数据制作两种方法的比较图?那么,我毫不怀疑地接受你的回答。 @XinZhang 这是一个与熊猫相关的问题,sammywemmy 回答了它,而我只是分享了一个替代选项,如果需要一些流处理并且输入数据不适合内存,这可能会有所帮助。我会投票接受 sammy 的 :)【参考方案4】:

由于 3 列的 bin 相同,请使用来自 cat 访问器的 codes

%timeit df.groupby([df['a_bins'].cat.codes, df['b_bins'].cat.codes, df['c_bins'].cat.codes])['value'].mean()
1.82 ms ± 27.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)

【讨论】:

agg 会慢一点。 在我的真实案例中它们并不相同。上面的示例是为了简单起见,如代码注释中所述。

以上是关于如何加快pandas groupby bins的agg?的主要内容,如果未能解决你的问题,请参考以下文章

带有 bin 计数的 Pandas groupby

Pandas groupby、bin 和 average

Pandas groupby 应用执行缓慢

python制作分布图

pandas如何对value列数据进行分组groupby?

如何使用 Pandas groupby 在组上添加顺序计数器列