数据聚合与分组操作
pandas的聚合与分组操作是日常进行数据分析要用到的常用操作,相信很多人都很熟悉了。但是聚合分析能做到的远不止只是简单地计算组内的平均值,中位数等统计学数值。groupby
可以对组内执行其他变换,比如计算分位数的排名,标准化,线性回归,排位和子集选择,计算分位数分析和区间分析等等,只要你能想到的,返回pandas对象或者标量的操作都可以。
pandas聚合分析的原理
解析
pandas执行groupby
操作以后就会按照键进行分组,从上面那个图可以看到,执行按照key
字段进行分组后,相同的key
就会分到一组,这个时候组内进行某个聚合函数aggfunc
的操作后再将得到的结果纵向拼接到一起,得到最后的结果。因此,明白了groupby
的过程之后,我们大概也就明白,自定义的函数只要是能够对一个dataframe进行操作,并且返回一个pandas对象或者一个标量的,都是可以用这样一个函数的。
df = pd.DataFrame({'key': ['a', 'b', 'c'] * 4,
'value': numpy.arange(12.)})
df
g=df.groupby('key')
很多人不知道的是,pandas聚合后的对象其实是一个键值对的对象。
# 我们试一下
for i, v in g:
print(i, v)
a key value value_1
0 a 0.0 44
3 a 3.0 15
6 a 6.0 20
9 a 9.0 6
b key value value_1
1 b 1.0 4
4 b 4.0 29
7 b 7.0 10
10 b 10.0 40
c key value value_1
2 c 2.0 12
5 c 5.0 7
8 c 8.0 38
11 c 11.0 0
a
, b
和c
分别是各个分组的分组名,对应的值是各个分组的dataframe
。
自定义一个函数进行组内和组间的计算
# 新建一个dataframe
df = pd.DataFrame({'key1' : ['a', 'a', 'b', 'b', 'a'],
'key2' : ['one', 'two', 'one', 'two', 'one'],
'data1' : numpy.random.randn(5),
'data2' : numpy.random.randn(5)})
df
我们新建一个函数用作后面对group对象进行的计算。
def get_result(group):
return (group['data1'].sum()-group['data2'].mean())/group['data1'].sum()
按照key1
进行分组。
df.groupby('key1').apply(get_result)
如果
apply
里面的函数本身就有聚合操作返回一个标量的话,那么聚合应用该函数后返回的就是具体的值,而不是扩展到所有行,比如上面的sum
是在轴方向进行加和,也就是组内进行操作,而不是单个操作,因此apply后返回的是数值。
如果我们要做的计算是对分组内每个单元格进行的,那么返回的就是跟原本的dataframe一样大小的。
def get_result_single(group):
return (group['data1']-group['data2'].mean())/group['data1'].sum()
df.groupby('key1').apply(get_result_single)
在apply中传入自定义的函数的参数
很多时候我们总是习惯在group
后对某一列进行操作,但很多时候我们忘了,其实我们可以直接对group后的对象进行一个复杂函数操作,这个时候建议先把函数写好,后面再调用,且记得apply
里面可以传入该函数的参数
tips = pd.read_csv('examples/tips.csv')
tips.head(3)
在这里我们尝试按照
smoker
这一列进行分组,并且排序找出分组后的total_bill
前5位最高的记录,为了更加熟悉apply
该函数可以传入自定义函数的参数的这个功能,我们自定义函数可以设定返回第几位,降序或者逆序。
def top(group, col, n=5, ascending=True):
return group.sort_values(by=col,ascending=ascending)[:n]
tips.groupby('smoker').apply(top, col='total_bill', n=5, ascending=True)
当然,根据自定义函数,我们还可以修改不同的参数,这样构建好一个函数,我们可以重复调用,这样既提高了效率,也增加了代码的可读性。
tips.groupby('smoker').apply(top, col='tip', n=3, ascending=False)
以上我们就按照组内的
tip
列进行降序排列,并筛选出前3行。
分组内自定义填充缺失值
我们有很多时候需要根据分组后不同的组别进行填充相应的值,在这里我们可以用自定义函数和字典的方式进行自定义。
我们把之前的df
的2, 4行改成缺失值,我们尝试用分组内的平均值来对应填充,使用fillna
方法
df.iloc[[1,3],[-2,-1]]=numpy.nan
df
def fill_mean(group):
return group.fillna(df.mean())
df.groupby('key1').apply(fill_mean)
以上我们填充的是组内的平均数,也就是调用了mean()
方法,但是如果我们要填充任意自定义的值的话,需要指定分组名和填充值的字典,然后用group对象的name
属性去字典取出对应的值。
fill_val={'a':-1, 'b':12}
fill_med=lambda g:g.fillna(fill_val[g.name])
df.groupby('key1').apply(fill_med)
[站外图片上传中...(image-959b14-1566618910459)]
g.name
相当于是分组后的索引的分组名,然后我们在字典中根据分组名去填充对应的值。
分箱后进行聚合
使用pd.cut
和pd.qcut
后,得到的是一个categorical
对象,这个对象就像是一个列表或者series
,可以传入给group
对象后面进行分组的依据。
tips.head()
如果我们尝试对total_bill进行分箱,然后分组
bins=pd.cut(tips.total_bill,bins=10)#返回一个series,这个series可以传入到groupby当中作为一个分组的依据
tips.groupby(bins).agg({'tip':'mean'})
按照字典或者列表来聚合
people = pd.DataFrame(numpy.random.randn(5, 5),
columns=['a', 'b', 'c', 'd', 'e'],
index=['Joe', 'Steve', 'Wes', 'Jim', 'Travis'])
people.iloc[2:3, [1, 2]] = numpy.nan # Add a few NA values
people
我们还可以传入一个字典,字典的名对应列名或者索引名,值代表新的分类,按照新的分类进行聚合。
mapping={'a':'good', 'b':'good', 'c':'bad', 'd':'good', 'e':'bad'}
people.groupby(mapping, axis=1).agg('mean')
当然我们也可以传入列表作为聚合的依据,类似于前面的categorical
操作,需要注意的是,列表的长度需要和长度或者宽度一致。比如我们这里有5行,我们按照行方向进行聚合的话,创建一个5个元素的列表,按照这个列表里面的值进行聚合。
label=['west']*3+['east']*2
label
people.groupby(label).agg({'a':'mean'})
两个例子
聚合后计算相关性
如果有这种情况,我们希望能够分组,并且分组后计算其他列与某列的相关性,比如在下面,我们想计算其他变量total_bill
,size
与tip
的相关性大小,我们可以考虑下怎么做。
tips.head()
通常来说,
series
对series
的单个相关性可以用series
的内置方法corr
来计算,如下:
tips['total_bill'].corr(tips['tip'])
dataframe
对series
的单个相关性可以用dataframe
的内置方法corrwith
来计算。
tips[['total_bill', 'tip']].corrwith(tips['size'])
现在加上分组
tips.groupby('smoker').apply(lambda x:x[['total_bill', 'tip']].corrwith(x['size']))
聚合后计算线性回归
正如之前所说,只要是能够返回pandas对象的函数都是可以用到apply
函数中的,statsmodels
的线性回归模块也不例外,返回的结果的params
参数就是一个series
。
import statsmodels.api as sm
def regression(group, x_val, y_val):
x=group[x_val]
x['intercept']=1
Y=group[y_val]
model=sm.OLS(Y,x)
results=model.fit()
return results.params
tips.groupby('smoker').apply(regression, x_val=['total_bill'], y_val='tip')
在列方向进行聚合
我们还可以在列方向进行聚合,如果是多层索引,指定索引名,按照对应索引名进行聚合。
columns = pd.MultiIndex.from_arrays([['US', 'US', 'US', 'JUPYTER NOTEBOOK', 'JUPYTER NOTEBOOK'],
[1, 3, 5, 1, 3]],
names=['cty', 'tenor'])
hier_df = pd.DataFrame(numpy.random.randn(4, 5), columns=columns)
hier_df
hier_df.groupby(level='cty',axis=1).mean()
hier_df.groupby(level='tenor',axis=1).mean()
总结
虽然以上似乎介绍了很多pandas聚合操作的用法,但我细细想起来,其实是再简单不过的操作罢了,关于想要玩好pandas的聚合操作,有这么以下几点记住即可。
- pandas分组后的是有
name
属性的分组对象,聚合函数是对各个分组的dataframe进行操作的,只要是能够返回一个pandas对象或者标量的函数都是可以应用到groupby以后的聚合里面的。 - pandas传入
groupby
里面的参数可以是series
,list
,dict
和categorical
。前面都比较好理解,categorical
是pandas的另外一种类型,目前主要应用在分桶上。 - pandas既可以按照组内的值进行聚合,也可以按照多层索引的层级进行聚合,传入的参数是
level='level_name'
;既可以在行方向进行聚合(默认),也可以在列方向进行聚合(传入axis=1
)的操作。 - 对于一个复杂一些的函数,可以先把函数和相关的参数写好,建议把常用的函数直接保存到剪贴板增强工具里面,后面再进行重复的调用,这样效率很高,而且代码很整洁。
只要理解上面这么几点,基本上都可以把groupby
用得比较遛了。个人到现在觉得,之所以要把各种数据分析和数据挖掘的函数用法钻研得深透,并不是为了geek而geek,相反,是为了更好更快地探索数据,需要用的时候几行代码就实现,丝毫察觉不到这个过程实现的曲折,不需要使用的时候再疯狂去谷歌,这样效率和体验都很差。
网友评论