在数据分析时,经常需要将数据分成不同的群组,pandas中的groupby()函数可以完美地完成各种分组操作。

分组是根据DataFrame/Series的某个字段值,将该字段的值相等的行/列分到同一组中,每一个小组是一个新的DataFrame或Series。

groupby()也可以按DataFrame中的多个字段分组,当多个字段的值都相等时分到同一组。

groupby()经常与批处理函数apply()、聚合函数agg()等配合使用,实现对数据的多元处理。

groupby用法和参数介绍


groupby(self, by=None, axis=0, level=None, as_index=True, sort=True, group_keys=True, squeeze=no_default, observed=False, dropna=True):

  • by: 指定根据哪个/哪些字段分组,默认值是None,按多个字段分组时传入列表。by参数可以按位置参数的方式传入。

  • axis: 设置按列分组还是按行分组,0或index表示按列分组,1或columns表示按行分组,默认值为0。

  • level: 当DataFrame的索引为多重索引时,level参数指定用于分组的索引,可以传入多重索引中索引的下标(0,1…)或索引名,多个用列表传入。

level参数不能与by参数同时使用,如果两者同时存在,当by参数传入的是多重索引中的索引,则level不生效,当by参数传入的是DataFrame的列名,则报错。

  • as_index: 分组结果默认将分组列的值作为索引,如果按单列分组,结果默认是单索引,如果按多列分组,结果默认是多重索引。将as_index设置为False可以重置索引(0,1…)。

  • sort: 结果按分组列的值升序排列,将sort设置为False则不排序,可以提升性能。

  • dropna: 默认情况下,分组列的NaN在分组结果中不保留,将dropna设置为False,可以保留NaN分组。

其他三个参数不用关注,group_keys参数在源码中未使用,squeeze参数因为类型不兼容,官方已弃用,observed参数表示重设索引时,保留创建的全NaN行。

分组对象的内部结构


# coding=utf-8import pandas as pdimport numpy as npvip_df = pd.DataFrame({'isVip': ['vip', 'svip', 'member', 'vip', 'member', 'vip', 'svip'], 'gender': ['male', 'female', 'female', 'female', 'male', 'female', 'male'], 'age': [25, 30, 40, 25, 40, 18, 30], 'vipLevel': ['LV2', 'LV5', np.nan, 'LV3', 'LV2', 'LV2', 'LV3'], 'growValue': [180, 425, np.nan, 288, 190, 110, 240]})print(vip_df)grouped = vip_df.groupby('isVip')print(grouped)
isVipgenderage vipLevelgrowValue0 vipmale 25LV2180.01svipfemale 30LV5425.02memberfemale 40NaNNaN3 vipfemale 25LV3288.04membermale 40LV2190.05 vipfemale 18LV2110.06svipmale 30LV3240.0

groupby()分组得到的是一个DataFrameGroupBy对象,直接打印DataFrameGroupBy对象只能看到它的内存地址,看不到内部的结构。

for name, group in grouped:print(name)print(group)
memberisVipgenderage vipLevelgrowValue2memberfemale 40NaNNaN4membermale 40LV2190.0svipisVipgenderage vipLevelgrowValue1svipfemale 30LV5425.06svipmale 30LV3240.0vipisVipgenderage vipLevelgrowValue0 vipmale 25LV2180.03 vipfemale 25LV3288.05 vipfemale 18LV2110.0

for group in grouped:print(group)print(type(group), type(group[0]), type(group[1]))
('member', isVipgenderage vipLevelgrowValue2memberfemale 40NaNNaN4membermale 40LV2190.0)  ('svip', isVipgenderage vipLevelgrowValue1svipfemale 30LV5425.06svipmale 30LV3240.0)  ('vip', isVipgenderage vipLevelgrowValue0 vipmale 25LV2180.03 vipfemale 25LV3288.05 vipfemale 18LV2110.0)  

DataFrameGroupBy是一个可迭代对象,可以转换成list打印,也可以直接遍历打印出来。遍历出来的是一个个元组,每个元组对应一个分组,元组的第一个元素与分组列里的值对应,元组的第二个元素是分到当前小组的数据,是一个DataFrame。

DataFrameGroupBy对象的内部结构为:[(分组名1, 子DataFrame1), (分组名2, 子DataFrame2), …],相当于groupby()将DataFrame按字段值分成了多个小的DataFrame,然后将字段值和小的DataFrame用元组的方式保存在DataFrameGroupBy对象中。

print(grouped.groups)group_name = [gn for gn in grouped.groups.keys()]print(group_name)group = grouped.get_group(group_name[2])print('-'*40, '\n', group, sep='')
{'member': [2, 4], 'svip': [1, 6], 'vip': [0, 3, 5]}['member', 'svip', 'vip']----------------------------------------isVipgenderage vipLevelgrowValue0 vipmale 25LV2180.03 vipfemale 25LV3288.05 vipfemale 18LV2110.0

分组对象的groups属性可以返回分组信息,结果是一个形似字典的对象,由分组名和此分组数据在原DataFrame中的行索引组成。

借用groups可以提取出所有分组的分组名,分组对象的get_group()方法可以返回指定分组名的子DataFrame。

按多重索引分组


vip_multi_df = vip_df.set_index(['isVip', 'gender'])print('-'*40, '\n', vip_multi_df, sep='')
---------------------------------------- age vipLevelgrowValueisVipgender vipmale 25LV2180.0svip female 30LV5425.0member female 40NaNNaNvipfemale 25LV3288.0member male 40LV2190.0vipfemale 18LV2110.0svip male 30LV3240.0

# 按多重索引中的指定索引进行分组grouped = vip_multi_df.groupby(level='isVip')print('-'*40, '\n', grouped.mean(), sep='')# 按多重索引中除指定索引之外的索引分组grouped = vip_multi_df.groupby(level=vip_multi_df.index.names.difference(['gender']))print('-'*40, '\n', grouped.mean(), sep='')# 按多重索引中的多个索引分组grouped = vip_multi_df.groupby(level=[0, 1])print('-'*40, '\n', grouped.mean(), sep='')
----------------------------------------age growValueisVipmember40.000000190.000000svip30.000000332.500000vip 22.666667192.666667----------------------------------------age growValueisVipmember40.000000190.000000svip30.000000332.500000vip 22.666667192.666667----------------------------------------agegrowValueisVipgender member female40.0NaN male40.0190.0svip female30.0425.0 male30.0240.0vipfemale21.5199.0 male25.0180.0

level参数用于设置按多重索引中的指定索引分组,level传入的方式可以是索引name,也可以是索引在多重索引中的下标,还可以是排除某个索引外的其他索引。指定多个时用列表的方式传入。

grouped = vip_multi_df.groupby('gender')print('-'*40, '\n', grouped.mean(), sep='')grouped = vip_multi_df.groupby(['gender', 'age'])print('-'*40, '\n', grouped.mean(), sep='')
----------------------------------------age growValuegender female28.250000274.333333male31.666667203.333333----------------------------------------growValuegender age female 18 110.0 25 288.0 30 425.0 40 NaNmale 25 180.0 30 240.0 40 190.0

多重索引中的索引也可以传给groupby()的by参数,分组结果与将多重索引作为DataFrame的列是一样的。

如果用DataFrame的列作为分组列,多重索引会被转换成列保留在结果中。也可以用多重索引中的索引与列一起组合分组,相当于先对DataFrame重设索引再分组。

重置结果的索引


grouped = vip_multi_df.groupby(['isVip', 'gender'])print('-'*40, '\n', grouped.mean(), sep='')# 重设索引grouped = vip_multi_df.groupby(['isVip', 'gender'], as_index=False)print('-'*40, '\n', grouped.mean(), sep='')
----------------------------------------agegrowValueisVipgender member female40.0NaN male40.0190.0svip female30.0425.0 male30.0240.0vipfemale21.5199.0 male25.0180.0----------------------------------------agegrowValue040.0NaN140.0190.0230.0425.0330.0240.0421.5199.0525.0180.0

分组结果的索引默认是分组列的值,将as_index设置为False可以重置索引,相当于先分组再调用reset_index()函数。

结果是否排序


grouped = vip_df.groupby('isVip')print('-'*40, '\n', grouped.mean(), sep='')grouped = vip_df.groupby('isVip', sort=False)print('-'*40, '\n', grouped.mean(), sep='')
----------------------------------------age growValueisVipmember40.000000190.000000svip30.000000332.500000vip 22.666667192.666667----------------------------------------age growValueisVipvip 22.666667192.666667svip30.000000332.500000member40.000000190.000000

groupby()默认对结果按分组列的值升序排列,如果将sort参数修改为False,则不排序,保留原DataFrame中的顺序,不排序可以提升性能。

是否保留空值


grouped = vip_df.groupby('vipLevel')print('-'*40, '\n', grouped.mean(), sep='')grouped = vip_df.groupby('vipLevel', dropna=False)print('-'*40, '\n', grouped.mean(), sep='')
----------------------------------------agegrowValuevipLevelLV2 27.666667160.0LV3 27.500000264.0LV5 30.000000425.0----------------------------------------agegrowValuevipLevelLV2 27.666667160.0LV3 27.500000264.0LV5 30.000000425.0NaN 40.000000NaN

当分组列有空值(NaN)时,默认的分组结果中不保留NaN分组,将dropna参数修改为False,正常保留NaN分组。

提取分组结果的指定列


grouped = vip_df.groupby('isVip', dropna=False)print('-'*40, '\n', grouped['gender'], sep='')for name, group in grouped['gender']:print(name)print(group)
----------------------------------------member2female4maleName: gender, dtype: objectsvip1female6maleName: gender, dtype: objectvip0male3female5femaleName: gender, dtype: object

从groupby()分组结果中取一列,得到的是一个SeriesGroupBy对象,直接打印SeriesGroupBy对象只能看到它的内存地址,看不到内部的结构。

SeriesGroupBy的内部结构与DataFrameGroupBy相似,SeriesGroupBy对象的内部结构为:[(分组名1, 子Series1), (分组名2, 子Series2), …],因此SeriesGroupBy也可以转换成list打印,也可以遍历取出每一个元素。

grouped = vip_df['gender'].groupby(vip_df['isVip'])for name, group in grouped:print(name)print(group)
member2female4maleName: gender, dtype: objectsvip1female6maleName: gender, dtype: objectvip0male3female5femaleName: gender, dtype: object

也可以先指定需要获取的列,再按DataFrame的另一个列进行分组,结果与先分组再获取指定列相同。

以上就是pandas中groupby()函数的用法介绍和分析,本文都是用DataFrame举例,Series的用法相似,不重复了。