Pandas中GROUP BY HAVING的等价操作

82 投票
3 回答
70675 浏览
提问于 2025-04-17 20:23

在pandas中,使用groupby和并行应用过滤器的最有效方法是什么?

基本上,我在问这个操作在SQL中相当于什么。

select *
...
group by col_name
having condition

我认为有很多使用场景,比如条件平均值、总和、条件概率等等,这样的命令会非常强大。

我需要非常好的性能,所以理想情况下,这样的命令不应该是通过在python中进行多层操作得到的结果。

3 个回答

0

我按照州和县进行分组,筛选出最大值大于20的记录,然后使用数据框的loc方法对结果进行子查询,找出为真的值。

counties=df.groupby(['state','county'])['field1'].max()>20
counties=counties.loc[counties.values==True]
3

在pandas中,使用groupby和并行应用过滤的最有效方法是什么?

使用 groupby.transform + 布尔索引

虽然在pandas中有个类似的写法叫 groupby.filter,但它的速度非常慢。如果你在意性能,最好是在进行groupby操作后再过滤数据,而不是在操作过程中进行过滤。因为 groupby.filter 对每个组都要调用Python函数(比如lambda),而 groupby.transform 是对整个数据框调用一个经过Cython优化的函数,所以当组的数量很多时,后者的速度要快得多。

使用 groupby.transform 的好处是,它返回一个和原始数据框有相同索引的新数据框,里面填充的是聚合后的值。由于输出的索引和原始数据框相同,所以可以用来过滤原始数据框。

所以,

SELECT * FROM df GROUP BY colA HAVING COUNT(*) > 1

相当于

df[df.groupby('colA').transform('size') > 1]

SELECT * FROM df GROUP BY colA HAVING SUM(colB) > 5

相当于

df[df.groupby('colA')['colB'].transform('sum') > 5]

无论如何,正如下面的性能图所示,随着组的数量增加, groupby.transform + 布尔索引的速度远远快于 groupby.filter;例如,当组数达到1万时,它的速度快了1000倍。实际上,如果你的数据框有数百万个组, groupby.filter 可能根本无法运行,而 groupby.transform + 布尔索引会在合理的时间内完成工作。

result

用于生成上述图表的代码

import perfplot
import pandas as pd
import numpy as np

def groupby_filter(df):
    g = df.groupby('A')
    return g.filter(lambda x: x['B'].sum() > 5)

def groupby_transform(df):
    g = df.groupby('A')
    return df[g['B'].transform('sum') > 5]


perfplot.plot(
    kernels=[groupby_filter, groupby_transform],
    n_range=[2**k for k in range(16)],
    setup=lambda n: pd.DataFrame({
        'A': np.random.choice(n, size=n, replace=False), 
        'B': np.random.randint(n, size=n)}),
    xlabel='Number of groups'
)
103

正如unutbu的评论中提到的,groupby的过滤功能相当于SQL中的HAVING语句:

In [11]: df = pd.DataFrame([[1, 2], [1, 3], [5, 6]], columns=['A', 'B'])

In [12]: df
Out[12]:
   A  B
0  1  2
1  1  3
2  5  6

In [13]: g = df.groupby('A')  #  GROUP BY A

In [14]: g.filter(lambda x: len(x) > 1)  #  HAVING COUNT(*) > 1
Out[14]:
   A  B
0  1  2
1  1  3

你可以写更复杂的函数(这些函数会应用到每个组上),只要它们返回一个简单的布尔值(真或假):

In [15]: g.filter(lambda x: x['B'].sum() == 5)
Out[15]:
   A  B
0  1  2
1  1  3

注意:可能存在一个bug,就是你不能在用于分组的列上直接写函数... 一个解决办法是手动对这些列进行分组,比如 g = df.groupby(df['A']))

撰写回答