如何使pandas的groupby函数不变为惰性计算?

5
这个教程提到了pandas中的groupby对象是惰性的。
它的本质是懒惰的。在你明确要求之前,它并不会执行任何操作来产生有用的结果。
而且,值得一提的是,.groupby()确实会做一些分割工作,但不是全部,只是为你传递的每个键构建一个Grouping类实例。然而,这些分组所持有的BaseGrouper类的许多方法都是在init()时被调用的,而且许多方法也使用缓存属性设计。
因此,我进行了一些测试来确保groupby确实是惰性的。
让我们
df=pd.DataFrame(np.random.randint(1,10,size=(1000000,4)))

那么

%timeit gg=df.groupby(1)
35.6 µs ± 110 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)

几乎不需要花费时间,而且...
%timeit res=gg.get_group(1)
2.76 ms ± 8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

计时更长,仅比稍快

%timeit res=df[df[1]==1]
6.87 ms ± 16.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

另一方面,如果我们首先提取组

%timeit gdict=df.groupby(1).groups
15.7 ms ± 35.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

获取群组不需要任何时间

%timeit gdict[1]
29.8 ns ± 0.0989 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

所以我的问题是

  1. 为什么Pandas将groupby设计成懒惰的呢?在实际应用中,我认为几乎总是需要对组对象进行许多进一步的操作。如果组对象在第一次拆分数据帧时就是懒惰的,那么每次执行像get_group等操作时都会浪费时间。
  2. 我也不理解“.groupby()确实做了一些拆分工作,但并非全部,通过为您传递的每个键构建一个Grouping类实例来实现”,这是什么意思?
  3. 有可能使groupby对象不懒惰吗?
1个回答

1

你需要一个更大的基准:

import numpy as np, pandas as pd
df=pd.DataFrame(np.random.randint(1,10,size=(100000000,4))) #3GB data
gg=df.groupby(1)
%time _ = gg.get_group(1) #first call slow
%time _ = gg.get_group(1) #fast
%time _ = gg.get_group(2) #other group lookup is also fast 
%timeit _ = gg.get_group(1) #gives wrong result

Groupby 是一种懒加载方式,它不会立即计算 groups。只有在第一次请求它们时才会这样做。或者当您使用 IPython 并在光标下使用 gg 时按 tab 键。如果您跟踪进程的内存消耗,就可以看到它。或者在 IPython 的情况下可以感受到它。

很难猜测底层发生了什么,但是 get_group 似乎有自己的缓存,而 groups 和像 summin 这样的方法则共享一个缓存。可能是为了最小化不同用例的内存使用。无论如何,在第一次使用后,惰性都消失了。

最终测试是错误的。 gg.groups 包含索引,而不是组本身:

%timeit df.loc[gdict[1]]  #It is actually the slowest
1.23 s ± 26.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit df[df[1]==1]
928 ms ± 23.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit gg.get_group(1)
510 ms ± 30.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

从字典中检索项目确实快了数千倍,但是你会用空间换速度。

如果您绝对确定需要在同一组上运行函数多次,可以尝试按列对数据帧进行排序并保存组切片。

%time df = df.sort_values(1,ignore_index=True)
#Wall time: 10.3 s
%time ids = df[1].diff().to_numpy().nonzero()[0]
#Wall time: 1.88 s
%time gl = {df[1][v] : slice(v,ids[i+1] if (i+1)<len(ids) else None) for i,v in enumerate(ids)}
#Wall time: 112 µs
%timeit df[gl[1]]
#12.1 µs ± 208 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

对于某些用例,排序后的数据可能是最快的。

%timeit {k:df[v].sum() for k,v in gl.items()}
1.16 s ± 42.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit gg.sum()
2.73 s ± 29.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit {x: gg.get_group(x).sum() for x in range(1,10)}
4.23 s ± 61.9 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接