Pandas之--聚合技术(GroupBy技术)
1.分组运算
所谓的“分组运算”是多个步骤的一个组合,我们可以拆分为“split-apply-combine”(拆分-应用-合并),我觉得这个词很好的描述了整个过程。分组运算的第一个阶段,pandas对象(无论是Series,DataFrame还是其他的)中的数据会根据你所提供的一个或多个“key”,被拆分(split)为多个组。拆分操作是在对象的特定轴上执行的,例如,DataFrame可以在其行(axis = 0)或者列(axis = 1)上进行分组,然后,将一个函数应用(apply)到各个分组,并产生一个新值。最后,所有的这些函数的执行结果会被合并(combine)到最终的结果对象中。结果对象的形式一般取决于数据上所执行的操作。
注意:apply函数为聚合函数,例如,sum、mean、min、max等
下图展示了分组聚合的过程:
分组的key可以有多种形式,且类型不必相同:
- 1.列表或数组,但是其长度与待分组的轴是一样的。
- 2.表示DataFrame某个列明的值
- 3.字典或者Series,给出带分组轴上的值与分组名之间的对应关系。
- 4.函数,用于处理轴索引或者索引中的各个标签
** 注意:后三种只是快捷方式而已,其最终的目的仍然是产生一组用于拆分对象的值**
2.代码演示
如果觉得上面的东西看起来很抽象,不用担心,我将在下面给出大量示例。首先来看一下下面这个非常简单的表格型数据集(以DataFrame的形式给出)
import pandas as pd
import numpy as np
df = pd.DataFrame({'key1':['a','a','b','b','a'],
'key2':['one','two','one','two','one'],
'data1':np.random.randn(5),
'data2':np.random.randn(5)})
print(df)
运行结果:
key1 key2 data1 data2
0 a one 0.015108 0.304983
1 a two 2.054185 -0.009759
2 b one -1.057348 -1.703048
3 b two -3.696947 -0.788548
4 a one 1.452735 0.388301
- 如果想按照‘key1’进行分组,并计算data1列的平均值。实现该功能的方式很多,而我们这里要用的是:访问data1,并根据key1调用groupby:
grouped = df['data1'].groupby(df['key1'])
print(grouped)
运行结果:
<pandas.core.groupby.groupby.SeriesGroupBy object at 0x1a2308ddd8>
- 变量grouped是一个GroupBy对象,它实际上还没有进行任何计算,只是含有一些有关分组键df[‘key1’]的中间数据而已。换句话说,该对象已经有了接下来对各个分组执行运算所需要的一切信息。例如,我们可以调用GroupBy的mean方法来计算分组平均值:
get_mean = grouped.mean()
print(get_mean)
运行结果:
key1
a 1.174009
b -2.377148
Name: data1, dtype: float64
- 稍后会将会详细讲解
.mean()
的调用过程,这里最重要的是,数据(Series)根据分组键进行了聚合,产生了一个新的Series,其索引为key1列中的唯一值,之所以结果中的索引名称为key1,是因为原始的DataFrame的列df[‘key1’]就叫这个名字。 - 如果我们一次传入多个数组,就会得到不同的结果:
means = df['data1'].groupby([df['key1'],df['key2']]).mean()
print(means)
运行结果:
key1 key2
a one 0.733921
two 2.054185
b one -1.057348
two -3.696947
Name: data1, dtype: float64
- 这里,我通过对两个键对数据进行了分组,得到的Series具有一个层次化索引(由唯一的键对组成):
print(means.unstack())
运行结果:
key2 one two
key1
a 0.733921 2.054185
b -1.057348 -3.696947
- 在上面这些示例中,分组键均为Series。实际上,分组键可以是任何长度适当的数组:
states = np.array(['Ohio','califonia','califonia','Ohio','Ohio'])
years = np.array([2005,2005,2006,2005,2006])
df['data1'].groupby([states,years]).mean()
运行结果:
Ohio 2005 -1.840920
2006 1.452735
califonia 2005 2.054185
2006 -1.057348
Name: data1, dtype: float64
- 此外,你还可以将列名(可以是字符串,数字或者其他Python对象)用作分组键:
print(df.groupby(['key1']).mean())
运行结果:
data1 data2
key1
a 1.174009 0.227842
b -2.377148 -1.245798
print(df.groupby(['key1','key2']).mean())
运行结果:
data1 data2
key1 key2
a one 0.733921 0.346642
two 2.054185 -0.009759
b one -1.057348 -1.703048
two -3.696947 -0.788548
- 你可能已经注意到了,在执行df.groupby(‘key’).mean()时,结果中没有key2列。这是因为df[‘key2’]不是数值数据(俗称“麻烦列”),所以被从结果中排除了。默认情况下,所有数值列都会被聚合,虽然有时可能会被过滤为一个子集(后面会介绍到)
- 无论你准备拿GroupBy做什么,都有可能会用到GroupBy的size方法,它可以返回一个含有分组大小的Series:
df.groupby(['key1','key2']).size()
运行结果:
key1 key2
a one 2
two 1
b one 1
two 1
dtype: int64
注意:目前为止,分组键中的任何缺失值都会被排除在结果之外,但是后面的版本也许会对缺失值进行相应的处理
3.对分组进行迭代
GroupBy对象支持迭代,可以产生一组二元元祖(由分组和数据块组成)。看看下面的一个简单例子:
for name,group in df.groupby('key1'):
print(name)
print(group)
运行结果:
a
key1 key2 data1 data2
0 a one 0.015108 0.304983
1 a two 2.054185 -0.009759
4 a one 1.452735 0.388301
b
key1 key2 data1 data2
2 b one -1.057348 -1.703048
3 b two -3.696947 -0.788548
- 对于多重键的情况,元祖的第一个元素将会是有键值组成的元祖:
for (k1,k2),group in df.groupby(['key1','key2']):
print(k1,k2)
print(group)
运行结果:
a one
key1 key2 data1 data2
0 a one 0.015108 0.304983
4 a one 1.452735 0.388301
a two
key1 key2 data1 data2
1 a two 2.054185 -0.009759
b one
key1 key2 data1 data2
2 b one -1.057348 -1.703048
b two
key1 key2 data1 data2
3 b two -3.696947 -0.788548
- 当然,你可以对这些数据片段做任何其他的操作。有一个你可能会觉得有用的运算:将这些数据片段做成一个字典。
pieces = dict(list(df.groupby('key1')))
print("pieces:\n",pieces)
print("pieces['b']:\n",pieces['b'])
运行结果:
pieces:
{'a': key1 key2 data1 data2
0 a one 0.015108 0.304983
1 a two 2.054185 -0.009759
4 a one 1.452735 0.388301, 'b': key1 key2 data1 data2
2 b one -1.057348 -1.703048
3 b two -3.696947 -0.788548}
pieces['b']:
key1 key2 data1 data2
2 b one -1.057348 -1.703048
3 b two -3.696947 -0.788548
- groupby默认是在axis = 0 上进行分组的,通过设置也可以在其他任何轴上进行分组。拿上面的例子的df来说,我们可以根据dtype对列进行分组:
grouped = df.groupby(df.dtypes,axis= 1)
dict(list(grouped))
运行结果:
{dtype('float64'): data1 data2
0 0.015108 0.304983
1 2.054185 -0.009759
2 -1.057348 -1.703048
3 -3.696947 -0.788548
4 1.452735 0.388301,
dtype('O'): key1 key2
0 a one
1 a two
2 b one
3 b two
4 a one}
- 可以看到分组后的结果,是按照数据的类型分组的。并且是按照列进行分组的
4.選取一個或一組列
對於由DataFrame產生的GroupBy對象,如果用一個(單個字符串)或一組(字符串數組)列名對其進行索引,就能實現選取部分列進行聚合的目的。也就是說:
df.groupby('key1')['data1']
df.groupby('key1')[['data2']]
是以下代码的语法糖:
df['data1'].groupby(df['key1'])
df[['data2']].groupby(df['key1'])
- 尤其对于大数据集,很可能只需要对部分列进行聚合。例如,在前面那个数据集中,如果只需要计算data2列的平均值并以DataFrame形式得到结果,我们可以编写:
s = df.groupby(['key1','key2'])[['data2']].mean()
print(s)
运行结果:
data2
key1 key2
a one 0.346642
two -0.009759
b one -1.703048
two -0.788548
- 这种索引操作所返回的对象是一个已经分组的DataFrame(如果传入的是列表或者数组)或者已经分组的Series(如果传入的是标量形式的单个列名):
s_grouped = df.groupby(['key1','key2'])['data2']
print(s_grouped)
print(s_grouped.mean())
运行结果:
<pandas.core.groupby.groupby.SeriesGroupBy object at 0x1a1ff968d0>
key1 key2
a one 0.346642
two -0.009759
b one -1.703048
two -0.788548
Name: data2, dtype: float64
5.通过字典或者Series进行分组
除数组以外,分组信息还可以由其他形式存在。看另一个示例DataFrame:
people = pd.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值
print(people)
"""
a b c d e
Joe 0.485915 -1.271190 0.090832 -0.237905 -1.414645
Steve 0.659409 -0.406590 -0.985230 0.429787 1.351408
Wes -1.043782 NaN NaN 0.379168 0.095054
Jim 0.059189 -0.966218 -1.253383 1.774299 1.461221
Travis 1.702478 0.331087 0.568426 -0.985880 0.586774
"""
- 假设已知列的分组关系,并希望根据分组计算列的总计:
# 定义一个字典映射
mapping = {'a':'red','b':'red','c':'blue','d':'blue','e':'red','f':'orange'}
# 根据映射去进行分组
by_colum = people.groupby(mapping,axis=1)
# 分组后求和
print(by_colum.sum())
"""
blue red
Joe -0.147073 -2.199920
Steve -0.555443 1.604226
Wes 0.379168 -0.948727
Jim 0.520916 0.554192
Travis -0.417454 2.620338
"""
- Series也有同样的功能,它可以被看做一个固定大小的映射,对于上面那个例子,如果用Series作为分组键,则pandas会检查Series以确保期索引分组轴是对齐的:
map_series = pd.Series(mapping)
by_series = people.groupby(map_series,axis= 1).count()
print(by_series)
"""
blue red
Joe 2 3
Steve 2 3
Wes 1 2
Jim 2 3
Travis 2 3
"""
6.通过函数进行分组
相较于字典或者Series,Python函数在定义分组映射关系时,可以更有创意且更为抽象。任何被当做分组键的函数都会在各个索引值上被调用一次,其返回值就会被用作分组名称,具体点说,以上一小节的示例DataFrame为例,其索引值为人的名字。假设你希望根据人名的长度进行分组,虽然可以求取一个字符串长度数组,但其实仅仅传入len函数就可以了:
s = people.groupby(len).sum()
print(s)
"""
a b c d e
3 -0.498678 -2.237408 -1.162551 1.915562 0.141630
5 0.659409 -0.406590 -0.985230 0.429787 1.351408
6 1.702478 0.331087 0.568426 -0.985880 0.586774
"""
- 将函数跟数组、列表、字典、Series混合使用也是可以的,因为任何东西最后都会被转化为数组:
key_list = ['one','one','one','two','two']
s = people.groupby([len,key_list]).min()
print(s)
"""
a b c d e
3 one -1.043782 -1.271190 0.090832 -0.237905 -1.414645
two 0.059189 -0.966218 -1.253383 1.774299 1.461221
5 one 0.659409 -0.406590 -0.985230 0.429787 1.351408
6 two 1.702478 0.331087 0.568426 -0.985880 0.586774
"""
- 如果你足够细心,会发现本来有5行数据,怎么现在只有4行,没错,其中有NAN的一行被干掉了!
7.根据索引级别分组
层次化索引数据集最方便的地方就在于它能够根据索引级别进行聚合。要实现该目的,通过level关键字传入级别编号或名称即可:
columns = pd.MultiIndex.from_arrays([['US','US','US','JP','JP'],
[1,3,5,1,3]],
names = ['city','tenor'])
hier_df = pd.DataFrame(np.random.randn(4,5),columns = columns)
print(hier_df)
"""
city US JP
tenor 1 3 5 1 3
0 0.700936 1.110087 -1.058324 1.316239 -0.940866
1 0.193931 1.845167 0.191146 -1.081856 -1.396286
2 -0.533317 0.021661 1.216783 -0.777967 -1.105844
3 0.759767 -0.599406 -1.145386 -0.675289 0.465727
"""
- 可以看到定义好的带有层次索引的DataFrame,下面使用层次索引进行分组:
s = hier_df.groupby(level='city',axis=1).count()
print(s)
"""
city JP US
0 2 3
1 2 3
2 2 3
3 2 3
"""
8.总结
这就是这个GroupBy得使用方法,重点在“key”的使用,本篇演示了多种key的定义方式,可以根据不同的业务需求选择!