学生体测的数据集上,如果想要按照性别统计身高中位数,就可以如下写出:
df = pd.read_csv('../data/learn_pandas.csv')
df.groupby('Gender')['Height'].median()
#Gender
#,Female 159.6
#,Male 173.4
#,Name: Height, dtype: float64
前面提到的若干例子都是以单一维度进行分组的,比如根据性别,如果现在需要根据多个维度进行分组,该如何做?事实上,只需在groupby中传入相应列名构成的列表即可。例如,现希望根据学校和性别进行分组,统计身高的均值就可以如下写出:
df.groupby(['School', 'Gender'])['Height'].mean()
#School Gender
#,Fudan University Female 158.776923
#, Male 174.212500
#,Peking University Female 158.666667
#, Male 172.030000
#,Shanghai Jiao Tong University Female 159.122500
#, Male 176.760000
#,Tsinghua University Female 159.753333
#, Male 171.638889
#,Name: Height, dtype: float64
groupby的分组依据都是直接可以从列中按照名字获取的,那如果希望通过一定的复杂逻辑来分组,例如根据学生体重是否超过总体均值来分组,同样还是计算身高的均值。
condition = df.Weight > df.Weight.mean()
df.groupby(condition)['Height'].mean()
#Weight
#,False 159.034646
#,True 172.705357
#,Name: Height, dtype: float64
从索引可以看出,其实最后产生的结果就是按照条件列表中元素的值(此处是True和False)来分组,下面用随机传入字母序列来验证这一想法:
item = np.random.choice(list('abc'), df.shape[0])
df.groupby(item)['Height'].mean()
#a 163.094828
#,b 163.874603
#,c 162.666129
#,Name: Height, dtype: float64
此处的索引就是原先item中的元素,如果传入多个序列进入groupby,那么最后分组的依据就是这两个序列对应行的唯一组合:
df.groupby([condition, item])['Height'].mean()
#Weight
#,False a 159.334146
#, b 159.257143
#, c 158.543182
#,True a 172.164706
#, b 173.109524
#, c 172.744444
#,Name: Height, dtype: float64
由此可以看出,之前传入列名只是一种简便的记号,事实上等价于传入的是一个或多个列,最后分组的依据来自于数据来源组合的unique值,通过drop_duplicates就能知道具体的组类别:
df[['School', 'Gender']].drop_duplicates()
# School Gender
#0 Shanghai Jiao Tong University Female
#1 Peking University Male
#2 Shanghai Jiao Tong University Male
#3 Fudan University Female
#4 Fudan University Male
#5 Tsinghua University Female
#9 Peking University Female
#16 Tsinghua University Male
df.groupby([df['School'], df['Gender']])['Height'].mean()
#School Gender
#,Fudan University Female 158.776923
#, Male 174.212500
#,Peking University Female 158.666667
#, Male 172.030000
#,Shanghai Jiao Tong University Female 159.122500
#, Male 176.760000
#,Tsinghua University Female 159.753333
#, Male 171.638889
#,Name: Height, dtype: float64
能够注意到,最终具体做分组操作时,所调用的方法都来自于pandas中的groupby对象,这个对象上定义了许多方法,也具有一些方便的属性
gb = df.groupby(['School', 'Grade'])
gb
通过ngroups属性,可以得到分组个数
gb.ngroups
#16
通过 groups 属性,可以返回从 组名 映射到 组索引列表 的字典
res = gb.groups
res.keys() # 字典的值由于是索引,元素个数过多,此处只展示字典的键
#dict_keys([('Fudan University', 'Freshman'), ('Fudan University', 'Junior'),
# ('Fudan University', 'Senior'), ('Fudan University', 'Sophomore'),
# ('Peking University', 'Freshman'), ('Peking University', 'Junior'),
# ('Peking University', 'Senior'), ('Peking University', 'Sophomore'),
# ('Shanghai Jiao Tong University', 'Freshman'), ('Shanghai Jiao Tong University', 'Junior'),
# ('Shanghai Jiao Tong University', 'Senior'), ('Shanghai Jiao Tong University', 'Sophomore'),
# ('Tsinghua University', 'Freshman'), ('Tsinghua University', 'Junior'),
# ('Tsinghua University', 'Senior'), ('Tsinghua University', 'Sophomore')])
当size作为DataFrame的属性时,返回的是表长乘以表宽的大小,但在groupby对象上表示统计每个组的元素个数
gb.size()
#School Grade
#,Fudan University Freshman 9
#, Junior 12
#, Senior 11
#, Sophomore 8
#,Peking University Freshman 13
#, Junior 8
#, Senior 8
#, Sophomore 5
#,Shanghai Jiao Tong University Freshman 13
#, Junior 17
#, Senior 22
#, Sophomore 5
#,Tsinghua University Freshman 17
#, Junior 22
#, Senior 14
#, Sophomore 16
#,dtype: int64
通过get_group方法可以直接获取所在组对应的行,此时必须知道组的具体名字:
gb.get_group(('Fudan University', 'Freshman'))
# School Grade Name Gender Height Weight Transfer Test_Number Test_Date Time_Record
#15 Fudan University Freshman Changqiang Yang Female 156.0 49.0 N 3 2020/1/1 0:05:25
#28 Fudan University Freshman Gaoqiang Qin Female 170.2 63.0 N 2 2020/1/7 0:05:24
#63 Fudan University Freshman Gaofeng Zhao Female 152.2 43.0 N 2 2019/10/31 0:04:00
#70 Fudan University Freshman Yanquan Wang Female 163.5 55.0 N 1 2019/11/19 0:04:07
#73 Fudan University Freshman Feng Wang Male 176.3 74.0 N 1 2019/9/26 0:03:31
#105 Fudan University Freshman Qiang Shi Female 164.5 52.0 N 1 2019/12/11 0:04:23
#108 Fudan University Freshman Yanqiang Xu Female 152.4 38.0 N 1 2019/12/8 0:05:03
#157 Fudan University Freshman Xiaoli Lv Female 152.5 45.0 N 2 2019/9/11 0:04:17
#186 Fudan University Freshman Yanjuan Zhao Female NaN 53.0 N 2 2019/10/9
熟悉了一些分组的基本知识后,重新回到开头举的三个例子,可能会发现一些端倪,即这三种类型分组返回的数据型态并不一样:
第一个例子中,每一个组返回一个标量值,可以是平均值、中位数、组容量size等 第二个例子中,做了原序列的标准化处理,也就是说每组返回的是一个Series类型 第三个例子中,既不是标量也不是序列,返回的整个组所在行的本身,即返回了DataFrame类型
由此,引申出分组的三大操作:聚合、变换和过滤,分别对应了三个例子的操作,下面就要分别介绍相应的agg、transform和filter函数及其操作。
在介绍agg之前,首先要了解一些直接定义在groupby对象的聚合函数,因为它的速度基本都会经过内部的优化,使用功能时应当优先考虑。根据返回标量值的原则,包括如下函数:max/min/mean/median/count/all/any/idxmax/idxmin/mad/nunique/skew/quantile/sum/std/var/sem/size/prod
gb = df.groupby('Gender')['Height']
gb.idxmin()
#Gender
#,Female 143
#,Male 199
#,Name: Height, dtype: int64
gb.quantile(0.95)
#Gender
#,Female 166.8
#,Male 185.9
#,Name: Height, dtype: float64
虽然在groupby对象上定义了许多方便的函数,但仍然有以下不便之处:
无法同时使用多个函数 无法对特定的列使用特定的聚合函数 无法使用自定义的聚合函数 无法直接对结果的列名在聚合前进行自定义命名 下面说明如何通过agg函数解决这四类问题:
【a】使用多个函数
当使用多个聚合函数时,需要用列表的形式把内置聚合函数对应的字符串传入,先前提到的所有字符串都是合法的。
gb.agg(['sum', 'idxmax', 'skew'])
# Height Weight
# sum idxmax skew sum idxmax skew
#Gender
#Female 21014.0 28 -0.219253 6469.0 28 -0.268482
#Male 8854.9 193 0.437535 3929.0 2 -0.332393
从结果看,此时的列索引为多级索引,第一层为数据源,第二层为使用的聚合方法,分别逐一对列使用聚合,因此结果为6列。
【b】对特定的列使用特定的聚合函数
对于方法和列的特殊对应,可以通过构造字典传入agg中实现,其中字典以列名为键,以聚合字符串或字符串列表为值。
gb.agg({'Height':['mean','max'], 'Weight':'count'})
# Height Weight
# mean max count
#Gender
#Female 159.19697 170.2 135
#Male 173.62549 193.9 54
【c】使用自定义函数
在agg中可以使用具体的自定义函数, 需要注意传入函数的参数是之前数据源中的列,逐列进行计算。 下面分组计算身高和体重的极差:
gb.agg(lambda x: x.mean()-x.min())
# Height Weight
#Gender
#Female 13.79697 13.918519
#Male 17.92549 21.759259
【d】聚合结果重命名 如果想要对聚合结果的列名进行重命名,只需要将上述函数的位置改写成元组,元组的第一个元素为新的名字,第二个位置为原来的函数,包括聚合字符串和自定义函数,现举若干例子说明:
gb.agg([('range', lambda x: x.max()-x.min()), ('my_sum', 'sum')])
# Height Weight
# range my_sum range my_sum
#Gender
#Female 24.8 21014.0 29.0 6469.0
#Male 38.2 8854.9 38.0 3929.0
gb.agg({'Height': [('my_func', my_func), 'sum'], 'Weight': lambda x:x.max()})
# Height Weight
# my_func sum <lambda>
#Gender
#Female Low 21014.0 63.0
#Male High 8854.9 89.0
变换函数的返回值为同长度的序列,最常用的内置变换函数是累计函数:cumcount/cumsum/cumprod/cummax/cummin,它们的使用方式和聚合函数类似,只不过完成的是组内累计操作。此外在groupby对象上还定义了填充类和滑窗类的变换函数,这些函数的一般形式将会分别在第七章和第十章中讨论,此处略过。
gb.cummax().head()
# Height Weight
#0 158.9 46.0
#1 166.5 70.0
#2 188.9 89.0
#3 NaN 46.0
#4 188.9 89.0
当用自定义变换时需要使用transform方法,被调用的自定义函数, 其传入值为数据源的序列,与agg的传入类型是一致的,其最后的返回结果是行列索引与数据源一致的DataFrame。
现对身高和体重进行分组标准化,即减去组均值后除以组的标准差:
gb.transform(lambda x: (x-x.mean())/x.std()).head()
# Height Weight
#0 -0.058760 -0.354888
#1 -1.010925 -0.355000
#2 2.167063 2.089498
#3 NaN -1.279789
#4 0.053133 0.159631
前面提到了transform只能返回同长度的序列,但事实上还可以返回一个标量,这会使得结果被广播到其所在的整个组,这种标量广播的技巧在特征工程中是非常常见的。例如,构造两列新特征来分别表示样本所在性别组的身高均值和体重均值:
gb.transform('mean').head() # 传入返回标量的函数也是可以的
# Height Weight
#0 159.19697 47.918519
#1 173.62549 72.759259
#2 173.62549 72.759259
#3 159.19697 47.918519
#4 173.62549 72.759259
在上一篇中介绍了索引的用法,那么索引和过滤有什么区别呢?
过滤在分组中是对于组的过滤,而索引是对于行的过滤,在上一篇中的返回值,无论是布尔列表还是元素列表或者位置列表,本质上都是对于行的筛选,即如果符合筛选条件的则选入结果表,否则不选入。
组过滤作为行过滤的推广,指的是如果对一个组的全体所在行进行统计的结果返回True则会被保留,False则该组会被过滤,最后把所有未被过滤的组其对应的所在行拼接起来作为DataFrame返回。
在groupby对象中,定义了filter方法进行组的筛选,其中自定义函数的输入参数为数据源构成的DataFrame本身,在之前例子中定义的groupby对象中,传入的就是df[[‘Height’, ‘Weight’]],因此所有表方法和属性都可以在自定义函数中相应地使用,同时只需保证自定义函数的返回为布尔值即可。
例如,在原表中通过过滤得到所有容量大于100的组:
gb.filter(lambda x: x.shape[0] > 100).head()
Height Weight
#0 158.9 46.0
#3 NaN 41.0
#5 158.0 51.0
#6 162.5 52.0
#7 161.9 50.0
1.apply的引入
之前几节介绍了三大分组操作,但事实上还有一种常见的分组场景,无法用前面介绍的任何一种方法处理,例如现在如下定义身体质量指数BMI(见代码模块):
首先,这显然不是过滤操作,因此filter不符合要求;其次,返回的均值是标量而不是序列,因此transform不符合要求;最后,似乎使用agg函数能够处理,但是之前强调过聚合函数是逐列处理的,而不能够多列数据同时处理。由此,引出了apply函数来解决这一问题。
2.apply的使用
在设计上,apply的自定义函数传入参数与filter完全一致,只不过后者只允许返回布尔值。现如下解决上述计算问题:
def BMI(x):
Height = x['Height']/100
Weight = x['Weight']
BMI_value = Weight/Height**2
return BMI_value.mean()
gb.apply(BMI)
#Gender
#,Female 18.860930
#,Male 24.318654
#,dtype: float64
参考:阿里云天池