本文是DataWhale组队学习pandas的总结。
一、分组模式及其对象
1. 分组的一般模式
想要利用pandas实现分组操作,必须明确三个要素: 分组依据 、 数据来源 、 操作及其返回结果 。同时从充分性的角度来说,如果明确了这三方面,就能确定一个分组操作,从而分组代码的一般模式即:
df.groupby(分组依据)[数据来源].使用操作
这里利用MovieLens-1M数据集做分析。
原始数据集部分展示如下:
import numpy as np
import pandas as pd
读取文件,分隔符为’::’
movie_df = pd.read_csv('../data/ratings.dat',sep="::",engine='python',names=['user_id','item_id','rating','Timestamp'])
movie_df
列名分别表示用户ID,物品ID,用户对物品的评分以及评分时间戳。
如果我们要按照用户id统计用户对其看过的电影评分的中位数:
movie_df.groupby('user_id')['rating'].median()
如果我们要按照用户id统计用户对其看过的电影评分的平均数:
movie_df.groupby('user_id')['rating'].mean()
如果我们要统计每一部电影的平均评分(这里的评分是指看过这部电影的用户们给这部电影打出的评分,求其平均数大致可以体现这部电影的质量):
movie_df.groupby('item_id')['rating'].mean()
从结果可以分析得出电影1具有较高的评分平均数,也能体现出这部电影的口碑不错,相较之下电影4的评分平均数相对较低,口碑一般。
2. 分组依据的本质
前面提到的若干例子都是以单一维度进行分组的,比如根据用户ID,物品ID,如果现在需要根据多个维度进行分组,该如何做?事实上,只需在groupby中传入相应列名构成的列表即可。例如,现想根据user_id和rating进行分组,统计item_id最大的电影就可以如下写出:
movie_df.groupby(['user_id','rating'])['item_id'].max()
从上述结果也可以看出用户1偏好于给出较高的电影评分,用户2则喜欢给出较低的电影评分。
目前为止,groupby的分组依据都是直接可以从列中按照名字获取的,那如果想要通过一定的复杂逻辑来分组,例如根据评分是否超过评分总体均值来分组,并计算分组后的评分均值。
首先应该先写出分组条件:
condition = movie_df.rating > movie_df.rating.mean()
movie_df.groupby(condition)['rating'].mean()
从索引可以看出,其实最后产生的结果就是按照条件列表中元素的值(此处是True和False)来分组,下面用随机传入字母序列来验证这一想法:
index = np.random.choice(list('abc'),movie_df.shape[0])
movie_df.groupby(index)['rating'].mean()
movie_df.groupby([condition,index])['rating'].mean()
由此可以看出,之前传入列名只是一种简便的记号,事实上等价于传入的是一个或多个列,最后分组的依据来自于数据来源组合的unique值,通过drop_duplicates就能知道具体的组类别:
movie_df[['user_id','rating']].drop_duplicates()
3. Groupby对象
能够注意到,最终具体做分组操作时,所调用的方法都来自于pandas中的groupby对象,这个对象上定义了许多方法,也具有一些方便的属性。
gb = movie_df.groupby(['user_id','rating'])
gb
通过ngroups属性,可以访问分为了多少组:
gb.ngroups # --->28370
通过groups属性,可以返回从 组名 映射到 组索引列表 的字典:
res = gb.groups
res.keys() # 字典的值由于是索引,元素个数过多,此处只展示字典的键
当size作为DataFrame的属性时,返回的是表长乘以表宽的大小,但在groupby对象上表示统计每个组的元素个数:
gb.size()
通过get_group方法可以直接获取所在组对应的行,此时必须知道组的具体名字:
gb.get_group((1,3))
二.聚合函数
1. 内置聚合函数
在介绍agg之前,首先要了解一些直接定义在groupby对象的聚合函数,因为它的速度基本都会经过内部的优化,使用功能时应当优先考虑。根据返回标量值的原则,包括如下函数:max/min/mean/median/count/all/any/idxmax/idxmin/mad/nunique/skew/quantile/sum/std/var/sem/size/prod。
计算能够获取到最小值的索引位置(整数)。idxmin()
gb = movie_df.groupby('user_id')['rating']
gb.idxmin()
上述结果显示的就是每个用户对电影的最低评分的索引位置。(索引从0开始,原始数据集索引是从1开始,所以索引是1对应的就是数据集中第二条数据)
这些聚合函数当传入的数据来源包含多个列时,将按照列进行迭代计算:
gb = movie_df.groupby('user_id')[['rating','Timestamp']]
gb.min()
2. agg方法
虽然在groupby对象上定义了许多方便的函数,但仍然有以下不便之处:
1.无法同时使用多个函数
2.无法对特定的列使用特定的聚合函数
3.无法使用自定义的聚合函数
4.无法直接对结果的列名在聚合前进行自定义命名
下面说明如何通过agg函数解决这四类问题:
【a】使用多个函数
当使用多个聚合函数时,需要用列表的形式把内置聚合函数的对应的字符串传入,先前提到的所有字符串都是合法的。
gb = movie_df.groupby('user_id')[['rating','Timestamp']]
gb.agg(['sum', 'idxmax', 'skew'])
从结果看,此时的列索引为多级索引,第一层为数据源,第二层为使用的聚合方法,分别逐一对列使用聚合,因此结果为6列。
【b】对特定的列使用特定的聚合函数
对于方法和列的特殊对应,可以通过构造字典传入agg中实现,其中字典以列名为键,以聚合字符串或字符串列表为值。
gb.agg({'rating':['mean','max'],'Timestamp':'min'})
【c】使用自定义函数
在agg中可以使用具体的自定义函数, 需要注意传入函数的参数是之前数据源中的列,逐列进行计算 。下面分组计算rating和Timestamp的极差:
gb.agg(lambda x: x.mean()-x.min())
由于传入的是序列,因此序列上的方法和属性都是可以在函数中使用的,只需保证返回值是标量即可。下面的例子是指,如果组的指标均值,超过该指标的总体均值,返回High,否则返回Low。
def my_func(s):
res = 'High'
if s.mean() <= movie_df[s.name].mean():
res = 'Low'
return res
gb.agg(my_func)
【d】聚合结果重命名
如果想要对结果进行重命名,只需要将上述函数的位置改写成元组,元组的第一个元素为新的名字,第二个位置为原来的函数,包括聚合字符串和自定义函数,现举若干例子说明:
gb.agg([('range', lambda x: x.max()-x.min()), ('my_sum', 'sum')])
三、变换和过滤
1. 变换函数与transform方法
变换函数的返回值为同长度的序列,最常用的内置变换函数是累计函数:cumcount/cumsum/cumprod/cummax/cummin,它们的使用方式和聚合函数类似,只不过完成的是组内累计操作。此外在groupby对象上还定义了填充类和滑窗类的变换函数。
gb.cummax().head()
当用自定义变换时需要使用transform方法,被调用的自定义函数, 其传入值为数据源的序列 ,与agg的传入类型是一致的,其最后的返回结果是行列索引与数据源一致的DataFrame。
现对raing和Timestamp进行分组标准化,即减去组均值后除以组的标准差:
gb.transform(lambda x: (x-x.mean())/x.std()).head()
2. 组索引与过滤
组过滤作为行过滤的推广,指的是如果对一个组的全体所在行进行统计的结果返回True则会被保留,False则该组会被过滤,最后把所有未被过滤的组其对应的所在行拼接起来作为DataFrame返回。
在groupby对象中,定义了filter方法进行组的筛选,其中自定义函数的输入参数为数据源构成的DataFrame本身,在之前例子中定义的groupby对象中,传入的就是df[[‘Height’, ‘Weight’]],因此所有表方法和属性都可以在自定义函数中相应地使用,同时只需保证自定义函数的返回为布尔值即可。
gb.filter(lambda x: x.shape[0] > 100).head()