Pandas实践_分组

2023-12-14 05:04:08


一、分组模式及其对象

1.分组的一般模式

学生体测的数据集上,如果想要按照性别统计身高中位数,就可以如下写出:

df = pd.read_csv('../data/learn_pandas.csv')
df.groupby('Gender')['Height'].median()
#Gender
#,Female    159.6
#,Male      173.4
#,Name: Height, dtype: float64

2.分组依据的本质

前面提到的若干例子都是以单一维度进行分组的,比如根据性别,如果现在需要根据多个维度进行分组,该如何做?事实上,只需在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

3.Groupby对象

能够注意到,最终具体做分组操作时,所调用的方法都来自于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	

4.分组的三大操作

熟悉了一些分组的基本知识后,重新回到开头举的三个例子,可能会发现一些端倪,即这三种类型分组返回的数据型态并不一样:

第一个例子中,每一个组返回一个标量值,可以是平均值、中位数、组容量size等 第二个例子中,做了原序列的标准化处理,也就是说每组返回的是一个Series类型 第三个例子中,既不是标量也不是序列,返回的整个组所在行的本身,即返回了DataFrame类型

由此,引申出分组的三大操作:聚合、变换和过滤,分别对应了三个例子的操作,下面就要分别介绍相应的agg、transform和filter函数及其操作。

二、聚合函数

1.内置聚合函数

在介绍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

2.agg方法

虽然在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

三、变换和过滤

1.变换函数与transform方法

变换函数的返回值为同长度的序列,最常用的内置变换函数是累计函数: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

2.组索引与过滤

在上一篇中介绍了索引的用法,那么索引和过滤有什么区别呢?

过滤在分组中是对于组的过滤,而索引是对于行的过滤,在上一篇中的返回值,无论是布尔列表还是元素列表或者位置列表,本质上都是对于行的筛选,即如果符合筛选条件的则选入结果表,否则不选入。

组过滤作为行过滤的推广,指的是如果对一个组的全体所在行进行统计的结果返回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

参考:阿里云天池

文章来源:https://blog.csdn.net/weixin_42504788/article/details/134907941
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。