序 最近在学习Pandas,在处理数据时,经常需要对数据的某些字段进行分组分析,这就需要用到groupby函数,这篇文章做一个详细记录 Pandas版本1。4。3 Pandas中的groupby函数先将DataFrame或Series按照关注字段进行拆分,将相同属性划分为一组,然后可以对拆分后的各组执行相应的转换操作,最后返回汇总转换后的各组结果一、基本用法 先初始化一些数据,方便演示importpandasaspddfpd。DataFrame({name:〔香蕉,菠菜,糯米,糙米,丝瓜,冬瓜,柑橘,苹果,橄榄油〕,category:〔水果,蔬菜,米面,米面,蔬菜,蔬菜,水果,水果,粮油〕,price:〔3。5,6,2。8,9,3,2。5,3。2,8,18〕,count:〔2,1,3,6,4,8,5,3,2〕}) 按category分组groupeddf。groupby(category)print(type(grouped))print(grouped) 输出结果classpandas。core。groupby。generic。DataFrameGroupBypandas。core。groupby。generic。DataFrameGroupByobjectat0x127112df0 grouped的类型是DataFrameGroupBy,直接尝试输出,打印是内存地址,不太直观,这里写一个函数来展示(可以这么写的原理,后面会介绍)defviewgroup(thepdgroup):forname,groupinthepdgroup:print(fgroupname:{name})print(30)print(group)print(30,)viewgroup(grouped) 输出结果groupname:水果namecategorypricecount0香蕉水果3。526柑橘水果3。257苹果水果8。03groupname:米面namecategorypricecount2糯米米面2。833糙米米面9。06groupname:粮油namecategorypricecount8橄榄油粮油18。02groupname:蔬菜namecategorypricecount1菠菜蔬菜6。014丝瓜蔬菜3。045冬瓜蔬菜2。58二、参数源码探析 接下来看一下源码中的方法定义DataFrame的groupbydefgroupby(self,byNone,axis:Axis0,level:LevelNoneNone,asindex:boolTrue,sort:boolTrue,groupkeys:boolTrue,squeeze:boollib。NoDefaultnodefault,observed:boolFalse,dropna:boolTrue,)DataFrameGroupBy:pass Series的groupbydefgroupby(self,byNone,axis0,levelNone,asindex:boolTrue,sort:boolTrue,groupkeys:boolTrue,squeeze:boollib。NoDefaultnodefault,observed:boolFalse,dropna:boolTrue,)SeriesGroupBy:pass Series的groupby函数操作与DataFrame类似,这篇文章只以DataFrame作为示例入参by 再来回忆一下基本用法里的写法groupeddf。groupby(category) 这里传入的category就是第1个参数by,表示要按照什么进行分组,根据官方文档介绍,by可以是mapping,function,label,listoflabels中的一种,这里是用的label,也就是说,还可以像下面这样写label列表groupeddf。groupby(〔category〕)mapping 这种方式需要按DataFrame的index进行映射,这里把水果和蔬菜划分到大组蔬菜水果,米面和粮油划分到大组米面粮油categorydict{水果:蔬菜水果,蔬菜:蔬菜水果,米面:米面粮油,粮油:米面粮油}themap{}foriinrange(len(df。index)):themap〔i〕categorydict〔df。iloc〔i〕〔category〕〕groupeddf。groupby(themap)viewgroup(grouped) 输出结果如下groupname:米面粮油namecategorypricecount2糯米米面2。833糙米米面9。068橄榄油粮油18。02groupname:蔬菜水果namecategorypricecount0香蕉水果3。521菠菜蔬菜6。014丝瓜蔬菜3。045冬瓜蔬菜2。586柑橘水果3。257苹果水果8。03function 这种方式下,自定义函数的入参也是DataFrame的index,输出结果与mapping的例子相同categorydict{水果:蔬菜水果,蔬菜:蔬菜水果,米面:米面粮油,粮油:米面粮油}deftobigcategory(theidx):returncategorydict〔df。iloc〔theidx〕〔category〕〕groupeddf。groupby(tobigcategory)viewgroup(grouped)axis axis表示以哪个轴作为分组的切分依据 0等价于index,表示按行切分,默认 1等价于columns,表示按列切分 这里看一下按列切分的示例defgroupcolumns(columnname:str):ifcolumnnamein〔name,category〕:returnGroup1else:returnGroup2等价写法groupeddf。head(3)。groupby(groupcolumns,axiscolumns)groupeddf。head(3)。groupby(groupcolumns,axis1)viewgroup(grouped) 输出结果如下groupname:Group1namecategory0香蕉水果1菠菜蔬菜2糯米米面groupname:Group2pricecount03。5216。0122。83 相当于把表从垂直方向上切开,左半部分为Group1,右半部分为Group2level 当axis是MultiIndex(层级结构)时,按特定的level进行分组,注意这里的level是int类型,从0开始,0表示第1层,以此类推 构造另一组带MultiIndex的测试数据thearrays〔〔A,A,A,B,A,A,A,B,A,A〕,〔蔬菜水果,蔬菜水果,米面粮油,休闲食品,米面粮油,蔬菜水果,蔬菜水果,休闲食品,蔬菜水果,米面粮油〕,〔水果,蔬菜,米面,糖果,米面,蔬菜,蔬菜,饼干,水果,粮油〕〕theindexpd。MultiIndex。fromarrays(arraysthearrays,names〔one,two,three〕)df2pd。DataFrame(data〔3。5,6,2。8,4,9,3,2。5,3。2,8,18〕,indextheindex,columns〔price〕)print(df2) 输出结果如下priceonetwothreeA蔬菜水果水果3。5蔬菜6。0米面粮油米面2。8B休闲食品糖果4。0A米面粮油米面9。0蔬菜水果蔬菜3。0蔬菜2。5B休闲食品饼干3。2A蔬菜水果水果8。0米面粮油粮油18。0 1。按第3层分组groupeddf2。groupby(level2)viewgroup(grouped) 输出结果如下groupname:水果priceonetwothreeA蔬菜水果水果3。5水果8。0groupname:米面priceonetwothreeA米面粮油米面2。8米面9。0groupname:粮油priceonetwothreeA米面粮油粮油18。0groupname:糖果priceonetwothreeB休闲食品糖果4。0groupname:蔬菜priceonetwothreeA蔬菜水果蔬菜6。0蔬菜3。0蔬菜2。5groupname:饼干priceonetwothreeB休闲食品饼干3。2 共6个分组 2。按第1,2层分组groupeddf2。groupby(level〔0,1〕)viewgroup(grouped) 输出结果如下groupname:(A,米面粮油)priceonetwothreeA米面粮油米面2。8米面9。0粮油18。0groupname:(A,蔬菜水果)priceonetwothreeA蔬菜水果水果3。5蔬菜6。0蔬菜3。0蔬菜2。5水果8。0groupname:(B,休闲食品)priceonetwothreeB休闲食品糖果4。0饼干3。2 共3个分组,可以看到,分组名称变成了元组asindex bool类型,默认值为True。对于聚合输出,返回对象以分组名作为索引groupedself。df。groupby(category,asindexTrue)print(grouped。sum()) asindex为True的输出结果如下pricecountcategory水果14。710米面11。89粮油18。02蔬菜11。513groupedself。df。groupby(category,asindexFalse)print(grouped。sum()) asindex为False的输出结果如下,与SQL的groupby输出风格相似categorypricecount0水果14。7101米面11。892粮油18。023蔬菜11。513sort bool类型,默认为True。是否对分组名进行排序,关闭自动排序可以提高性能。注意:对分组名排序并不影响分组内的顺序groupkeys bool类型,默认为True 如果为True,调用apply时,将分组的keys添加到索引中squeeze 1。1。0版本已废弃,不解释observed bool类型,默认值为False 仅适用于任何groupers是分类(Categoricals)的 如果为True,仅显示分类分组的观察值;如果为False,显示分类分组的所有值dropna bool类型,默认值为True,1。1。0版本新增参数 如果为True,且分组的keys中包含NA值,则NA值连同行(axis0)列(axis1)将被删除 如果为False,NA值也被视为分组的keys,不做处理返回值 DateFrame的gropuby函数,返回类型是DataFrameGroupBy,而Series的groupby函数,返回类型是SeriesGroupBy 查看源码后发现他们都继承了BaseGroupBy,继承关系如图所示 BaseGroupBy类中有一个grouper属性,是ops。BaseGrouper类型,但BaseGroupBy类没有init方法,因此进入GroupBy类,该类重写了父类的grouper属性,在init方法中调用了grouper。py的getgrouper,下面是抽取出来的伪代码 groupby。py文件classGroupBy(BaseGroupBy〔NDFrameT〕):grouper:ops。BaseGrouperdefinit(self,。。。):。。。ifgrouperisNone:frompandas。core。groupby。grouperimportgetgroupergrouper,exclusions,objgetgrouper(。。。) grouper。py文件defgetgrouper(。。。)tuple〔ops。BaseGrouper,frozenset〔Hashable〕,NDFrameT〕:。。。createtheinternalsgroupergrouperops。BaseGrouper(groupaxis,groupings,sortsort,mutatedmutated,dropnadropna)returngrouper,frozenset(exclusions),objclassGrouping:obj:DataFrameorSeriesdefinit(self,index:Index,grouperNone,obj:NDFrameNoneNone,levelNone,sort:boolTrue,observed:boolFalse,inaxis:boolFalse,dropna:boolTrue,):pass ops。py文件classBaseGrouper:ThisisaninternalGrouperclass,whichactuallyholdsthegeneratedgroups。。。。。。definit(self,axis:Index,groupings:Sequence〔grouper。Grouping〕,。。。):。。。self。groupings:list〔grouper。Grouping〕list(groupings)propertydefgroupings(self)list〔grouper。Grouping〕:returnself。groupings BaseGrouper中包含了最终生成的分组信息,是一个list,其中的元素类型为grouper。Grouping,每个分组对应一个Grouping,而Grouping中的obj对象为分组后的DataFrame或者Series 在第一部分写了一个函数来展示groupby返回的对象,这里再来探究一下原理,对于可迭代对象,会实现iter()方法,先定位到BaseGroupBy的对应方法classBaseGroupBy:grouper:ops。BaseGrouperfinaldefiter(self)Iterator〔tuple〔Hashable,NDFrameT〕〕:returnself。grouper。getiterator(self。selectedobj,axisself。axis) 接下来进入BaseGrouper类中classBaseGrouper:defgetiterator(self,data:NDFrameT,axis:int0)Iterator〔tuple〔Hashable,NDFrameT〕〕:splitterself。getsplitter(data,axisaxis)keysself。groupkeysseqforkey,groupinzip(keys,splitter):yieldkey,group。finalize(data,methodgroupby) Debug模式进入group。finalize()方法,发现返回的确实是DataFrame对象 三、4大函数 有了上面的基础,接下来再看groupby之后的处理函数,就简单多了agg 聚合操作是groupby后最常见的操作,常用来做数据分析 比如,要查看不同category分组的最大值,以下三种写法都可以实现,并且grouped。aggregate和grouped。agg完全等价,因为在SelectionMixin类中有这样的定义:aggaggregate 但是要聚合多个字段时,就只能用aggregate或者agg了,比如要获取不同category分组下price最大,count最小的记录 还可以结合numpy里的聚合函数importnumpyasnpgrouped。agg({price:np。max,count:np。min}) 常见的聚合函数如下 聚合函数 功能 max 最大值 mean 平均值 median 中位数 min 最小值 sum 求和 std 标准差 var 方差 count 计数 其中,count在numpy中对应的调用方式为np。sizetransform 现在需要新增一列pricemean,展示每个分组的平均价格 transform函数刚好可以实现这个功能,在指定分组上产生一个与原df相同索引的DataFrame,返回与原对象有相同索引且已填充了转换后的值的DataFrame,然后可以把转换结果新增到原来的DataFrame上 示例代码如下groupeddf。groupby(category,sortFalse)df〔pricemean〕grouped〔price〕。transform(mean)print(df) 输出结果如下 apply 现在需要获取各个分组下价格最高的数据,调用apply可以实现这个功能,apply可以传入任意自定义的函数,实现复杂的数据操作frompandasimportDataFramegroupeddf。groupby(category,asindexFalse,sortFalse)defgetmaxone(thedf:DataFrame):sortdfthedf。sortvalues(byprice,ascendingTrue)returnsortdf。iloc〔1,:〕maxpricedfgrouped。apply(getmaxone)maxpricedf 输出结果如下 filter filter函数可以对分组后数据做进一步筛选,该函数在每一个分组内,根据筛选函数排除不满足条件的数据并返回一个新的DataFrame 假设现在要把平均价格低于4的分组排除掉,根据transform小节的数据,会把蔬菜分类过滤掉groupeddf。groupby(category,asindexFalse,sortFalse)filteredgrouped。filter(lambdasubdf:subdf〔price〕。mean()4)print(filtered) 输出结果如下 四、总结 groupby的过程就是将原有的DataFrameSeries按照groupby的字段,划分为若干个分组DataFrameSeries,分成多少个组就有多少个分组DataFrameSeries。因此,在groupby之后的一系列操作(如agg、apply等),均是基于子DataFrameSeries的操作。理解了这点,就理解了Pandas中groupby操作的主要原理五、参考文档 Pandas官网关于pandas。DateFrame。groupby的介绍 Pandas官网关于pandas。Series。groupby的介绍