前言
大家好,我是潜心。上篇文章提到了Groupby,但其中举例的代码有点问题,在提取序列时用到了for循环,效率很慢,后来查找了官方文档,才明白apply的重要性,再次对Groupby进行深入并总结。
本文约2.1k字,预计阅读15分钟。
Groupby: split-apply-combine
Pandas中Groupby定义如下:
def groupby(by=None, axis=0, level=None, as_index=True, sort=True, group_keys=True, squeeze=False, observed=False)
def groupby(by=None, axis=0, level=None, as_index=True, sort=True, group_keys=True, squeeze=False, observed=False)
Groupby具体来说指的是涉及以下一个或多个步骤的过程:
- 分割(Splitting):根据一些标准将数据划分为多个组。
- 应用(Applying):独立地对每个组应用一个函数。
- 组合(Combing):将结果组合成数据结构。
其中,分割是最直接的,也是最常用的(如上图)。但是事实上,在许多情况下,我们可能希望将数据集分成组,并对这些组做一些统计值计算。在Applying中,可能希望做以下其中一项:
- 聚集(Aggregation):计算每个组的汇总统计信息(或统计信息),例如计算分组的和、均值、个数等。
- 转换(Transformation):执行一些特定组的计算并返回一个like-indexed对象。
- 过滤(Filteration):根据判断为真或假的组计算,丢弃一些组。
此次我们不讨论Group的分割问题,主要聚焦它的Applying,并结合实际的大数据量的实际表来比较它们的效率问题。采用的数据表为用户与他点击的广告,共100w条:
user_id | creative_id | |
0 | 30920 | 567330 |
1 | 30920 | 3072255 |
2 | 30920 | 3072255 |
3 | 30920 | 3879497 |
4 | 30920 | 3751623 |
... | ... | ... |
Aggregation
当创建了GroupBy对象,根据需求我们可以对分组的数据执行计算。最简单的是我们通过agg()
方法来调用一些Python内置函数进行聚合计算,常用的内置函数为:mean
、median
、sum
、size
、count
、std
、describe
、min
、max
等。
这里我们基于user_id
对数据进行划分,简单应用部分内置函数,统计对每个用户他点击过的最大和最小的广告id:
%%time
In[1]: df.groupby('user_id').agg(['count', 'min', 'max'])
CPU times: user 72 ms, sys: 0 ns, total: 72 ms
Wall time: 70.9 ms
Out[1]:
user_id count min max
31 26 109090 4440651
34 63 3983 4266235
36 19 67988 3999372
310 12 10039 3042631
312 28 24918 3904071
... ... ... ...
363305 16 42555 4187970
363311 19 187530 4151703
363317 15 63052 4307786
363318 41 7400 4079814
363319 167 10257 4389117
30025 rows × 3 columns
%%time
In[1]: df.groupby('user_id').agg(['count', 'min', 'max'])
CPU times: user 72 ms, sys: 0 ns, total: 72 ms
Wall time: 70.9 ms
Out[1]:
user_id count min max
31 26 109090 4440651
34 63 3983 4266235
36 19 67988 3999372
310 12 10039 3042631
312 28 24918 3904071
... ... ... ...
363305 16 42555 4187970
363311 19 187530 4151703
363317 15 63052 4307786
363318 41 7400 4079814
363319 167 10257 4389117
30025 rows × 3 columns
以上我们发现索引变为了user_id
,若不想改变DataFrame的索引,则需要在groupby方法中的参数as_index
设置为False
。
当然如果你需要进一步进行统计运算,则panda允许提供多个lambdas
自定义计算。在这种情况下,panda将混淆lambda函数的名称,将_
附加到每个后续的lambda。可以通过rename()
更改列名。
%%time
In[2]: df.groupby('user_id').agg([lambda x: x.max() - x.min(), lambda x: x.mean() - x.median()])
CPU times: user 20.4 s, sys: 0 ns, total: 20.4 s
Wall time: 20.4 s
Out[2]:
creative_id
user_id 31 4331561 -107949.84615434 4262252 430452.82539736 3931384 -16653.473684310 3032592 -121110.916667312 3879153 208385.392857
... ... ...363305 4145415 364584.500000363311 3964173 524387.842105363317 4244734 787231.666667363318 4072414 -110147.829268363319 4378860 250463.10778430025 rows × 2 columns
%%time
In[2]: df.groupby('user_id').agg([lambda x: x.max() - x.min(), lambda x: x.mean() - x.median()])
CPU times: user 20.4 s, sys: 0 ns, total: 20.4 s
Wall time: 20.4 s
Out[2]:
creative_id
user_id 31 4331561 -107949.84615434 4262252 430452.82539736 3931384 -16653.473684310 3032592 -121110.916667312 3879153 208385.392857
... ... ...363305 4145415 364584.500000363311 3964173 524387.842105363317 4244734 787231.666667363318 4072414 -110147.829268363319 4378860 250463.10778430025 rows × 2 columns
对不同的列使用不同的聚合可以通过字典
的方式实现(这里采用文档的Code,C
、D
为列名):
In [3]: grouped.agg({'C': np.sum,
....: 'D': lambda x: np.std(x, ddof=1)})
....:
Out[3]:
C D
A
bar 0.392940 1.366330
foo -1.796421 0.884785
In [3]: grouped.agg({'C': np.sum,
....: 'D': lambda x: np.std(x, ddof=1)})
....:
Out[3]:
C D
A
bar 0.392940 1.366330
foo -1.796421 0.884785
Transformation
transform方法返回一个与正在分组的对象索引相同(大小相同)的对象。
我们通过对表进行与agg()
相同的内置函数进行比较:
%%time
In[4]: df.groupby('user_id').transform('count')
CPU times: user 28 ms, sys: 0 ns, total: 28 ms
Wall time: 29 ms
Out[4]:
creative_id
0 42
1 42
2 42
3 42
4 42
... ...
999995 167
999996 167
999997 167
999998 167
999999 167
1000000 rows × 1 columns
%%time
In[4]: df.groupby('user_id').transform('count')
CPU times: user 28 ms, sys: 0 ns, total: 28 ms
Wall time: 29 ms
Out[4]:
creative_id
0 42
1 42
2 42
3 42
4 42
... ...
999995 167
999996 167
999997 167
999998 167
999999 167
1000000 rows × 1 columns
我们发现返回了一个与原数据表大小相同的对象,并且把groupby
中的by
参数给省略了。(并且它不能一次使用多个内置函数)。
实际用途: 如果我们需要为原数据表添加一列count或其他内容,则需要使用该方法。
In[5]: df['ad_count'] = df.groupby('user_id').transform('count')
Out[5]:
user_id creative_id ad_count
0 30920 567330 42
1 30920 3072255 42
2 30920 2361327 42
3 30920 3879497 42
4 30920 3751623 42
... ... ... ...
999995 363319 121860 167
999996 363319 413801 167
999997 363319 415805 167
999998 363319 487803 167
999999 363319 655613 167
1000000 rows × 3 columns
In[5]: df['ad_count'] = df.groupby('user_id').transform('count')
Out[5]:
user_id creative_id ad_count
0 30920 567330 42
1 30920 3072255 42
2 30920 2361327 42
3 30920 3879497 42
4 30920 3751623 42
... ... ... ...
999995 363319 121860 167
999996 363319 413801 167
999997 363319 415805 167
999998 363319 487803 167
999999 363319 655613 167
1000000 rows × 3 columns
Filteration
filter方法是通过一些布尔判断对分组后的内容进行筛选,返回一个原始对象的子集。
假设我们需要过滤得到用户点击广告数小于100的样本:
%%time
In[6]: df.groupby('user_id').filter(lambda x: len(x) 100)
CPU times: user 7.48 s, sys: 0 ns, total: 7.48 s
Wall time: 7.49 s
Out[6]:
user_id creative_id ad_count
0 30920 567330 42
1 30920 3072255 42
2 30920 2361327 42
3 30920 3879497 42
4 30920 3751623 42
... ... ... ...
999828 363318 1779084 41
999829 363318 1985010 41
999830 363318 66606 41
999831 363318 1056195 41
999832 363318 1435072 41
838425 rows × 3 columns
%%time
In[6]: df.groupby('user_id').filter(lambda x: len(x) 100)
CPU times: user 7.48 s, sys: 0 ns, total: 7.48 s
Wall time: 7.49 s
Out[6]:
user_id creative_id ad_count
0 30920 567330 42
1 30920 3072255 42
2 30920 2361327 42
3 30920 3879497 42
4 30920 3751623 42
... ... ... ...
999828 363318 1779084 41
999829 363318 1985010 41
999830 363318 66606 41
999831 363318 1056195 41
999832 363318 1435072 41
838425 rows × 3 columns
可以发现,样本数目减少,每个用户点击广告数大于100的已被去除。
Apply
apply
方法相比更加灵活,它可以完成上述的agg、transform以及filter,具体取决于传递给它的是什么。并且它可以使用自定义函数。
例如返回每个用户最大的广告id(即agg方法)。
%%time
def change_ad_count(df):
return df['creative_id'].max()
In[7]: df.groupby('user_id').apply(change_ad_count)
CPU times: user 4.93 s, sys: 0 ns, total: 4.93 s
Wall time: 4.93 s
Out[7]:
user_id
31 4440651
34 4266235
36 3999372
310 3042631
312 3904071
...
363305 4187970
363311 4151703
363317 4307786
363318 4079814
363319 4389117
Length: 30025, dtype: int64
%%time
def change_ad_count(df):
return df['creative_id'].max()
In[7]: df.groupby('user_id').apply(change_ad_count)
CPU times: user 4.93 s, sys: 0 ns, total: 4.93 s
Wall time: 4.93 s
Out[7]:
user_id
31 4440651
34 4266235
36 3999372
310 3042631
312 3904071
...
363305 4187970
363311 4151703
363317 4307786
363318 4079814
363319 4389117
Length: 30025, dtype: int64
比较
因为上篇文章提到了Word2vec,通过每个用户所点击的广告来构造句子,因此需要将按照user_id
进行分组,并将每个用户点击的广告构造一个列表。之前用了for循环,效率极慢,现在改用agg
和apply
方法进行比较。
agg:
%%time
sentences = df.groupby(['user_id'])['creative_id'].agg(lambda x: x.tolist()).tolist()
CPU times: user 4.13 s, sys: 64 ms, total: 4.2 s
Wall time: 4.2 s
%%time
sentences = df.groupby(['user_id'])['creative_id'].agg(lambda x: x.tolist()).tolist()
CPU times: user 4.13 s, sys: 64 ms, total: 4.2 s
Wall time: 4.2 s
apply:
%%time
sentences = df.groupby(['user_id'])['creative_id'].apply(lambda x: x.tolist()).tolist()
CPU times: user 4.18 s, sys: 52 ms, total: 4.23 s
Wall time: 4.23 s
%%time
sentences = df.groupby(['user_id'])['creative_id'].apply(lambda x: x.tolist()).tolist()
CPU times: user 4.18 s, sys: 52 ms, total: 4.23 s
Wall time: 4.23 s
%%time
sentences = df.groupby(['user_id']).apply(lambda x: x['creative_id'].tolist()).tolist()
CPU times: user 2.32 s, sys: 44 ms, total: 2.37 s
Wall time: 2.37 s
%%time
sentences = df.groupby(['user_id']).apply(lambda x: x['creative_id'].tolist()).tolist()
CPU times: user 2.32 s, sys: 44 ms, total: 2.37 s
Wall time: 2.37 s
我们发现apply
方法与agg
相差无几,但我们若将creative_id
放入lambda中,则效率更高。个人认为应该是和处理的对象不同。
df.groupby(['user_id'])['creative_id']
0x7f1495e8d9e8>
df.groupby(['user_id'])0x7f1495e80a20>## apply中再处理Series对象
df.groupby(['user_id'])['creative_id']
0x7f1495e8d9e8>
df.groupby(['user_id'])0x7f1495e80a20>## apply中再处理Series对象
总结
groupby
是pandas最有效的方法之一,经常与agg
、transform
、filter
、apply
相结合使用。