相关文章推荐
调皮的凉面  ·  vue3 键盘事件 ...·  3 月前    · 
重情义的刺猬  ·  .net framework ...·  7 月前    · 
没读研的油条  ·  selenium基于登录HTTP ...·  8 月前    · 
数据聚合与分组运算

数据聚合与分组运算

5 年前 · 来自专栏 通往数据分析之路

对数据集进行分组并对各组应用一个函数,在数据分析工作中的重要环节。在数据准备好之后,通常的任务就是计算分组统计或生产透视表。pandas提供了一个高效灵活的groupby功能,可以完成对数据集进行切片、切块、摘要等操作。(比Excel中的透视表更为高级灵活)


和关系型数据库SQL中的group by 关键字相比,pandas中的groupby更高级灵活,可以实现对切分的数据块应用不局限于SQL聚合函数的其他函数,实现更灵活的数据聚合运算。

  • 根据一个或多个键(可以是函数、数组或者DataFrame列名)拆分pandas对象;
  • 计算分组摘要统计,如计数/平均值/标准差,或者自定义函数;
  • 对DataFrame的列应用各种各样的函数;
  • 应用组内转换活其他运算,如规格化,线性回归,排名或选取子集等
  • 技术透视表或交叉表
  • 执行分位数以及其他分组分析

GroupBy技术概念

Hadley Wickham创造了一个用于表示分组运算的术语“split—apply—combine”(拆分—应用—合并),分组运算第一阶段,pandas对象中的数据会根据你所提供的一个或多个键被拆分

(split)为多组,拆分操作是在对象特定轴上执行的。例如:DataFrame可以在其行(axis = 0)或者列(axis =1)上进行分组。然后,将一个函数应用(apply)到各个分组并产生一个新值。最后,所有这些函数的执行结果会被合并(combine)到最终结果对象中。


分组键可以有多种形式,且类型不必相同:

  • 列表活数组,其长度与待分组的轴一样;
  • 表示DataFrame某个列的值;
  • 字典或Series,给出待分组轴上的值与分组名之间的对应关系
  • 函数,用于处理轴索引活索引中的各个标签

分组groupby基本用法

import pandas as pd
import numpy as np
from pandas import Series,DataFrame
df = DataFrame({'key1':['a','a','b','b','a'],
               'key2':['one','two','one','two','one'],
               'data1':np.random.randn(5),
               'data2':np.random.randn(5)})
df

将df按key1进行分组,并计算data1列的平均值,此次方法为访问data1,并根据key1调用groupby。变量grouped是一个GroupBy对象,它其实只是完成了分组这一步,只是含有一些有关分组键df['key1']的中间数据而已,对各分组数据块应用mean函数,返回值合并对象。

grouped = df['data1'].groupby(df['key1'])
grouped.mean()

假如在groupbuy 传入一个列表,列表中有两个元素,则为根据两个键进行分组。

means = df['data1'].groupby([df['key1'],df['key2']]).mean()
means
means.unstack()  #unstack()方法将数据由一维转为二维,更容易查看理解,出栈

在上面例子中,分组键均为series,实际上,分组键可以时任何长度适应的数组:

states = pd.Series(['Ohio','calfornia','calfornia','Ohio','Ohio'])
years = pd.Series([2005,2006,2005,2005,2006])
df['data1'].groupby([states,years]).mean().unstack()  #分组键可以是与被分组列相等长的数组
df.groupby('key1').mean()   #传入列名,key2列不出现,因为已经忽略这个维度
df.groupby(['key1','key2']).mean()

使用groupby都可以用到size方法,返回一个含有分组大小的Series。

df.groupby(['key2','key1']).size()


对分组进行迭代

GroupBy对象支持迭代,可以产生一组二元元组(由分组名和数据块组成)

for name ,group in df.groupby('key1'):
    print name
    print group

对于多重键的情况,元组的第一个元素将会是由键值组成的元组。

for (k1,k2) ,group in df.groupby(['key1','key2']):
    print k1,k2
    print group

也可以对数据块转换成字典,使用dict和list实现。

pices =dict(list(df.groupby('key1')))#将数据片段进行操作,做成一个字典,然后可以使用分组键进行索引
pices['a']

groupby默认在axis=0上进行分组,通过设置也可以在其他任何轴上进行分组。


选取一个或一组列

对于有DataFrame产生的GroupBy对象,如果用一个或一组列名对其进行索引,就能实现选取部分列进行聚会的目的,即GroupBy对象也有类似DataFrame对象选取子集的方法。

通过对GroupBy对象索引的操作,返回的对象时一个已经分组的DataFrame或者已经分组的Series。

grouped_s = df.groupby(['key1','key2'])['data2']
grouped_s.mean()



通过字典或Series进行分组

people = DataFrame(np.random.randn(5,5),
                  columns=['a','b','c','d','e'],
                  index = ['Joe','Steve','Wes','Jim','Travis'])
people.ix[2:3,['b','c']] = np.nan #赋值几个Nan
people

假设已知列的分组关系,并希望根据分组计算列的总计:

mapping = {'a':'red','b':'red','c':'blue','d':'blue','e':'red','f':'orange'}
by_column =people.groupby(mapping,axis =1)
by_column.sum()

除了通过字典映射方式确定分组之外,Series也可以实现同样功能,它可以被看做一个固定大小的映射,如果用Series作为分组键,则pandas会检查Series以确保其索引跟分组轴的对齐方式。

map_series =Series(mapping)
map_series
people.groupby(map_series,axis=1).count()


通过函数进行分组

相教于字典或Series,Python函数在定义分组映射关系时可以更有创意且更为抽象。任何贝当作分组键的函数都会在各个索引值上被调用一次,且返回值就会被用作分组名称。具体来说,继续上面的people为例,其索引值为人名。假设你希望根据人名的长度进行分组,虽然可以求取一个字符串长度数组,但其实仅传入len函数就可以了。

people.groupby(len).count()
#len函数在各个索引值上调用一次,返回值就会作为分组名称,
#len将名字所以都过一遍,长度有3,5,6着三种,并作为分组依据

根据索引级别分组

层次化索引数据集最方便的地方就在于它能偶根据索引级别进行聚合,实现的方法就是通过level关键字传入级别编号或名称即可:

columns = pd.MultiIndex.from_arrays([['US','US','US','JP','JP'],
                                   [1,3,5,1,3]],names=['city','tenor'])
columns
hier_df = DataFrame(np.random.randn(4,5),columns =columns)
hier_df
hier_df.groupby(level='tenor',axis=1).count() #通过传入level的参数,可实现不同级别的分组

数据聚合

对于聚合,可认为是任何能够从数组产生标量值的数据转换过程。如SQL中的聚合函数sum(),count(),max()、min()、mean()等,然后在pandas中可以实现自定义的聚合运算,可以调用分组对象上已经定义好的任何方法,通过将定义好方法传入aggregate或agg方法即可:

df
def peak_to_peak(arr):                   #自定义一个函数
    return arr.max()-arr.min()
grouped.agg(peak_to_peak)               #GroupBy对象实现一遍函数,返回唯一值,聚合

经过优化后的GroupBy的方法(自定义的聚会函数计算过程相对要慢)

  • count:分组中非NA值的数理
  • sum:非NA值的和
  • mean:非NA值的平均值
  • median:非NA值的算术中位数
  • std、var:无偏(分母n-1)标准差和方差
  • min、max:非NA值最小和最大值
  • prod:非NA值的积
  • first、last :第一个和最后一个非NA值

面向列的多函数应用

假如需要聚合后数据GroupBy对象的不同的列使用不同的聚合函数,或一次应用多个函数。

tips =pd.read_csv('ch08/tips.csv')  #用pd的read_csv读取csv文件
tips['tips_pct'] = tips['tip']/tips['total_bill']   #增加一列小费占总额百分比
tips.head()   #使用head()查看前5条记录
grouped = tips.groupby(['sex','smoker'])
grouped_pct = grouped['tips_pct']
grouped_pct.agg('mean')             #对GroupBy对象选取‘tips_pct’进行mean函数应用

如果传入一组函数或函数名,得到的DataFrame的列就会以相应的函数命名

grouped_pct.agg(['mean','std',peak_to_peak])   #对GroupBy对象一次传入多个函数,则生产相应结果集

也可以通过传入一个由(name,function)元组组成的列表,各元组的一个元素就会贝用作DataFrame的列名。

grouped_pct.agg([('name1','mean'),('name2',np.std)]) 

对DataFrame可以通过定义一组应用于全部列的函数,或不同的列应用不同函数,假设我们想要对tip_pct和total_bill列计算三个统计信息(count,mean,max)

functions = ['count','mean','max']
result = grouped['tips_pct','total_bill'].agg(functions)
result

现在假设你想要对不同的列应用不同的函数,具体的方法是向agg传入一个从列名到函数的字典。

grouped.agg({'tip':np.max,'size':'sum'})
grouped.agg({'tips_pct':['min','max','mean','std'],
            'size':'sum'})  #对分组对象tips_pct列调用四个函数,对size调用一个sum函数

以无索引的形式返回聚合数据

以上的示例中的聚合数据都有由唯一的分组键组成的索引(可能还是层次化索引),如果通过传入as_index=False以禁用该功能,实现无索引形式返回聚合数据。

tips.groupby(['sex','smoker'],as_index=False).mean() #用sex和smoker进行分组,并且通过as_index=False,
                                                     #限制唯一分组键形式组成的索引,即可以实现重复方式

分组级运算和转换

聚合只不过是分组运算的其中一种而已,它是数据转换的一个特例,它实现了把一维数组简化为标量值的函数。下面将介绍transform和apply方法,他们可以实现更多其他的分组运算。


假设我们想要为一个DataFrame添加一个用于存放各索引分组平均值的列,一个办法是先聚合再合并;

df
k1_means =df.groupby('key1').mean().add_prefix('mean_')
k1_means
pd.merge(df,k1_means,left_on = 'key1',right_index=True) #通过pd.merge()方法合并数据集,关联方式左侧为‘key1’列,右侧是索引匹配

实现以上过程,更简单的方法是在GroupBy上使用transform方法;

key = ['one','two','one','two','one']
people.groupby(key).mean()    #使用上面的people数据
people.groupby(key).transform(np.mean)    #对索引按key进行分组,并且通过tranform把传入函数应用到各个分组,然后将结果放到适合的地方(重复展示)

transform也是有一个严格条件的特殊函数,传入的函数只能产生两种结果,要么产生一个可以广播的标量值,要么产生一个相同大小的结果数组 。

apply:一般性的“拆分——应用——合并”

最一般化的GroupBy方法时apply,可以将待处理对象拆分多个片段,然后对各个片段调用传入的函数,最后尝试将各片段组合到一起。

回到之前那个小费数据集,假设你想要根据分组选出最高的5个tip_pct值,首先,编写一个选取制定列具有最大值的行的函数:

def top(df,n=5,column='tips_pct'): #定义一个函数可应用到分组中选取5个最高的tips_pct值
    return df.sort_values(by=column)[-n:]
top(tips,n=6)

现在,如果对smoker分组并用该函数调用apply,就可以得到各分组的top5小费比例最高记录

tips.groupby('smoker').apply(top)  #按smoker分组,并返回每组top5,小费占比高的记录

top函数在DataFrame的各个片段上调用,然后结构由pandas.concat组装到一起,并以分组名称进行标记。于是,最终结果就是有了一个层次化索引,其内层索引来自原DataFrame。

tips.groupby(['smoker','day']).apply(top,n=1,column='total_bill')   
#按smoker和day分组,返回total_bill最高的一条记录,记是否吸烟的每天最高总计账单

调用describe函数就是用apply的快捷方式

f =lamdba x : x.describe()

grouped.apply(f)

result =tips.groupby('smoker')['tips_pct'].describe().stack()   
result
result.unstack('smoker')

禁止分组键

将group_key = False传入groupby中,即可禁止层次化索引,(默认Ture则分组键会跟原始对象的索引共同构成结果对象的层次化索引)。

tips.groupby(['sex','smoker'],group_keys =False).apply(top)


分位数和桶分析

在数据规整化中提过pandas有一些根据指定面元活样本分位数将数据拆分成多块的工具(如cut和qcut),将这些函数跟groupby结合起来,就能非常轻松的实现对数据集的桶(bucket)活分位数(quantile)分析了,以下面这个简单的随机数据集为例,我们将利用cut将其装入长度相等的桶中:

frame = DataFrame({'data1':np.random.randn(1000),
                  'data2':np.random.randn(1000)})
factor = pd.cut(frame.data1,4)