文章目录
前言
对数据集进行分类,并在每一组上应用一个聚合函数或转换函数,这通常是数据分析工作流中的一个重要部分。在载入、合并、准备数据集之后,你可能需要计算分组统计或者数据透视表用于报告或可视化的目的。pandas提供一个灵活的groupby接口,允许你以一种自然发方式对数据集进行切片、切块和总结。
我们通过Python和pandas的表达,我们可以使用pandas对象或NumPy数组执行相当复杂的组操作。在本章,我们将了解到:
- 使用一个或多个键(以函数、数组或DataFrame列名的形式)将pandas对象拆分为多块
- 计算组汇总统计信息,如计数、平均值或标准偏差或用户定义的函数
- 应用组内变换或其他操作,如标准化、线性回归、排位或子集选择
- 计算数据透视表和交叉表
- 执行分位数分析和其他统计组分析
一、GroupBy机制
描述组操作的术语为拆分-应用-联合。在操作的第一步,数据包含在pandas对象中,可以是Series、DataFrame或其他数据结构,之后根据你提供的一个或多个键分离到各个组中。分离操作是在数据对象的特定轴向上进行的。例如,DataFrame可以在它的行方向(axis=0)或列方向(axis=1)进行分组。分组操作后,一个函数就可以应用到各个组去,产生新的值,最终,所以函数的应用结果会联合为一个结果对象。结果对象的形式通常取决于对数据进行的操作:
分组键可是多种形式的,并且键不一定是完全相同的类型:
- 与需要分组的轴向长度一致的值列表或值数组
- DataFrame的列名的值
- 可以将分组轴向上的值和分组名称相匹配的字典或Series
- 可以在轴索引或索引中的单个标签上调用的函数
在下面的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 1.548485 -0.219585
1 a two 0.447387 -0.100247
2 b one 0.134934 -1.013706
3 b two -1.090257 0.071942
4 a one -1.650373 -0.496783
假设你想要根据key1标签计算data1列的均值,有多种方法可以实现。其中一种是访问data1并使用key1列(它是一个Series)调用groupby方法:
grouped = df['data1'].groupby(df['key1'])
print(grouped)
<pandas.core.groupby.generic.SeriesGroupBy object at 0x0000021CCB493100>
grouped变量现在是一个GroupBy对象。除了一些关于分组键df[‘key1’]的一些中间数据之外,它实际上还没有进行任何计算。这个对象拥有所有必需的信息,之后可以在每一个分组上应用一些操作。例如,为了计算分组的均值我们可以调用GroupBy的mean方法:
print(grouped.mean())
key1
a 0.115166
b -0.477662
Name: data1, dtype: float64
数据(一个Series)根据分组键进行了聚合,并产生了新的Series,这个Series使用key1列的唯一值作为索引。由于DataFrame的列df[‘key1’],结果中的索引名称是’key1‘。
如果我们将多个数组作为列表传入,则我们会得到一些不同的结果:
means = df['data1'].groupby([df['key1'],df['key2']]).mean()
print(means)
-----------------------------------------------------------------
key1 key2
a one -0.050944
two 0.447387
b one 0.134934
two -1.090257
Name: data1, dtype: float64
这里我们使用了两个键对数据进行分组,并且结果Series现在拥有一个包含唯一键对的多层索引:
print(means.unstack())
---------------------------------------------------------------------
key2 one two
key1
a -0.050944 0.447387
b 0.134934 -1.090257
在这个例子中,分组键都是Series,尽管分组键也可以是正确长度的任何数组:
states = np.array(['Ohio','California','California','Ohio','Ohio'])
years = np.array([2005,2005,2006,2005,2006])
a = df['data1'].groupby([states,years]).mean()
print(a)
--------------------------------------------------------------------------
California 2005 0.447387
2006 0.134934
Ohio 2005 0.229114
2006 -1.650373
Name: data1, dtype: float64
分组信息作为你想要继续处理的数据,通常包含在同一个DataFrame中。在这种情况下,你可以传递列名作为分组键:
a = df.groupby('key1').mean()
print(a)
a = df.groupby(['key1','key2']).mean()
print(a)
---------------------------------------------------------
data1 data2
key1
a 0.115166 -0.272205
b -0.477662 -0.470882
data1 data2
key1 key2
a one -0.050944 -0.358184
two 0.447387 -0.100247
b one 0.134934 -1.013706
two -1.090257 0.071942
第一行代码中df.grouby(‘key1’).mean()的结果里并没有key2列。这是因为df[‘key2’]并不是数值数据,即df[‘key2’]是一个冗余列,因此被排除在结果之外。默认情况下,所有的数值列都可以聚合。
如果不在意使用grouby的目的,通用的GroupBy方法是size,size返回一个包含组大小信息的Series:
a = df.groupby(['key1','key2']).size()
print(a)
-------------------------------------------------------------------
key1 key2
a one 2
two 1
b one 1
two 1
dtype: int64
1.1 遍历各分组
GroupBy对象支持迭代,会生成一个包含组名和数据块的2维元组序列。
for name,group in df.groupby('key1'):
print(name)
print(group)
----------------------------------------------------------
a
key1 key2 data1 data2
0 a one 1.548485 -0.219585
1 a two 0.447387 -0.100247
4 a one -1.650373 -0.496783
b
key1 key2 data1 data2
2 b one 0.134934 -1.013706
3 b two -1.090257 0.071942
在多个分组键的情况下,元组中的第一个元素是键值的元组:
for name,group in df.groupby(['key1','key2']):
print(name)
print(group)
-----------------------------------------------------------
('a', 'one')
key1 key2 data1 data2
0 a one 1.548485 -0.219585
4 a one -1.650373 -0.496783
('a', 'two')
key1 key2 data1 data2
1 a two 0.447387 -0.100247
('b', 'one')
key1 key2 data1 data2
2 b one 0.134934 -1.013706
('b', 'two')
key1 key2 data1 data2
3 b two -1.090257 0.071942
当然,你可以选择在任何一块数据上进行你想要的操作。使用一行代码计算出数据块的字典可能会对你有用:
a = dict(list(df.groupby('key1')))
print(a['b'])
------------------------------------------------------
key1 key2 data1 data2
2 b one 0.134934 -1.013706
3 b two -1.090257 0.071942
默认情况下,grouby在axis=0的轴向(行向)上分组,但你也可以在其他任意轴向上进行分组。例如,我们可以像以下代码一样,根据dtype对我们示例df的列进行分组:
a = df.dtypes
print(a)
------------------------------------------------------
key1 object
key2 object
data1 float64
data2 float64
dtype: object
grouped = df.groupby(df.dtypes,axis=1)
for dtypes,group in grouped:
print(dtypes)
print(group)
----------------------------------------------------
float64
data1 data2
0 1.548485 -0.219585
1 0.447387 -0.100247
2 0.134934 -1.013706
3 -1.090257 0.071942
4 -1.650373 -0.496783
object
key1 key2
0 a one
1 a two
2 b one
3 b two
4 a one
1.2 选择一列或所有列的子集
将从DataFrame创建的GrouBy对象用列名称或列名称数组进行索引时,会产生聚合的列子集的效果。例如:
df.groupby('key1')['data1']
df.groupby('key1')[['data2']]
与下面的语句是等价的:
df['data1'].groupby(df['key1'])
df[['data2']].groupby(df['key1'])
当我们要计算data2列的均值,并获得DataFrame形式的结果,我们可以写:
a = df.groupby(['key1','key2'])[['data2']].mean()
print(a)
------------------------------------------------------------------
data2
key1 key2
a one -0.358184
two -0.100247
b one -1.013706
two 0.071942
如果传递的是列表或数组,则此索引操作返回的对象是分组的DataFrame;如果只有单个列名作为标量传递,则为分组的Series:
a = df.groupby(['key1','key2'])['data2'].mean()
print(a)
-------------------------------------------------------------
key1 key2
a one -0.358184
two -0.100247
b one -1.013706
two 0.071942
Name: data2, dtype: float64
1.3 使用字典和Series分组
分组信息可能会以非数组形式存在。让我们考虑另一个示例DataFrame:
df = pd.DataFrame(np.random.randn(5,5),
columns=['a','b','c','d','e'],
index=['Joe','Steve','Wes','Jim','Travis'])
df.iloc[2:3,[1,2]]=np.nan
print(df)
-----------------------------------------------------------------
a b c d e
Joe 1.388421 -1.129679 0.075746 -1.894284 -0.715854
Steve -0.298438 -1.859657 -0.085582 -0.971794 -1.143098
Wes -0.124365 NaN NaN -1.528739 -0.409837
Jim 0.100342 -0.423482 0.909694 0.256412 -0.399447
Travis -1.620090 1.258952 -0.051295 1.331768 0.640878
现在我们假设拥有各列的分组对应关系,并且想把各列按组累加:
mapping = {'a':'red','b':'red','c':'blue','d':'blue','e':'red'}
by_column = df.groupby(mapping,axis=1)
print(by_column.sum())
----------------------------------------------------------------
blue red
Joe -1.818538 -0.457112
Steve -1.057376 -3.301192
Wes -1.528739 -0.534203
Jim 1.166107 -0.722587
Travis 1.280473 0.279740
Series也有相同的功能,可以视为固定大小的映射:
map_series = pd.Series(mapping)
a = df.groupby(map_series,axis=1).count()
print(a)
---------------------------------------------------------
blue red
Joe 2 3
Steve 2 3
Wes 1 2
Jim 2 3
Travis 2 3
1.4 使用函数分组
与使用字典或Series相比,使用Python函数是定义分组关系的一种更为通用的方式。作为分组键传递的函数将会按照每个索引值调用一次,同时返回值会被用作分组名称。更具体的来说,考虑上一节中的示例DataFrame,其中人的名字作为索引值。假设你想根据名字长度来进行分组。虽然你可以计算出字符串长度的数组,但传递len函数更为简单:
a = df.groupby(len).sum()
print(a)
------------------------------------------------------------------
a b c d e
3 1.364397 -1.553161 0.985440 -3.166611 -1.525138
5 -0.298438 -1.859657 -0.085582 -0.971794 -1.143098
6 -1.620090 1.258952 -0.051295 1.331768 0.640878
将函数与数组、字典或Series进行混合并不困难,所有对象都会在内部转换为数组:
key_list=['one','one','one','two','two']
a = df.groupby([len,key_list]).min()
print(a)
------------------------------------------------------------
a b c d e
3 one -0.124365 -1.129679 0.075746 -1.894284 -0.715854
two 0.100342 -0.423482 0.909694 0.256412 -0.399447
5 one -0.298438 -1.859657 -0.085582 -0.971794 -1.143098
6 two -1.620090 1.258952 -0.051295 1.331768 0.640878
1.5 根据索引层级分组
分层索引的数据集有一个非常方便的地方,就是能够在轴索引的某个层级上进行聚合。
columns=pd.MultiIndex.from_arrays([['US','US','US','JP','JP'],[1,3,5,1,3]],names=['cty','tenor'])
df = pd.DataFrame(np.random.randn(4,5),
columns=columns)
print(df)
-----------------------------------------------------------------------------
cty US JP
tenor 1 3 5 1 3
0 0.763263 -0.125198 0.072028 -0.670269 1.275435
1 -0.954808 -0.401308 -0.061292 -1.294746 -0.479677
2 -0.404107 0.014756 -2.206899 2.188720 0.594605
3 -1.047688 -0.753724 0.086409 0.652343 -0.438745
根据层级分组时,将层级数值或层级名称传递给level关键字:
a = df.groupby(level='cty',axis=1).count()
print(a)
------------------------------------------------------------
cty JP US
0 2 3
1 2 3
2 2 3
3 2 3
二、数据聚合
聚合是指所有根据数组产生标量值的数据转换过程。之前的例子已经使用了一些聚合操作,包括mean、count、min和sum等。很多常见的聚合,例如下表中的操作都有了优化实现。然而,你要用的可能并不局限于下面这个方法集。
函数名 | 描述 |
---|---|
count | 分组中非NA值数量 |
sum | 非NA值的累和 |
mean | 非NA值的均值 |
median | 非NA值的算术中位数 |
std、var | 无偏的(n-1分母)标准差和方差 |
min、max | 非NA值的最小值、最大值 |
prod | 非NA值的乘积 |
first、last | 非NA值的第一个和最后一个值 |
也可以使用自行制定的聚合,并再调用已经在分组对象上定义好的方法。例如,你可能还记得quantile可以计算Series或DataFrame列的样本分位数。尽管quantile并不是显式地为GroupBy对象实现的,但它是Series的方法,因此也可以用于聚合。在内部,GroupBy有效地对Series进行切片,为每一块调用piece.quantile(0.9),然后将这些结果一起组装到结果对象中:
print(df)
a = df.groupby('key1')['data1'].quantile(0.9)
print(a)
------------------------------------------------------
key1 key2 data1 data2
0 a one 1.548485 -0.219585
1 a two 0.447387 -0.100247
2 b one 0.134934 -1.013706
3 b two -1.090257 0.071942
4 a one -1.650373 -0.496783
key1
a 1.328265
b 0.012415
Name: data1, dtype: float64
要使用你自己的聚合函数,需要将函数传递给aggregate或agg方法:
def peak_to_peak(arr):
return arr.max()-arr.min()
a = df.groupby('key1').agg(peak_to_peak)
print(a)
-----------------------------------------------
data1 data2
key1
a 3.198858 0.396536
b 1.225191 1.085648
你可能会注意到一些方法,比如describe也是有效的,尽管严格来说它们并不是聚合函数:
a = df.groupby('key1').describe()
print(a)
-----------------------------------------------------
data1 ... data2
count mean std ... 50% 75% max
key1 ...
a 3.0 0.115166 1.625100 ... -0.219585 -0.159916 -0.100247
b 2.0 -0.477662 0.866341 ... -0.470882 -0.199470 0.071942
2.1 逐列及多函数应用
让我们回到上一章中的小费数据集。在使用read_csv载入数据集后,我们增加一个小费比例列tip_pct:
tips = pd.read_csv('D:\浏览器下载\pydata-book-2nd-edition\pydata-book-2nd-edition\examples/tips.csv')
tips['tip_pct'] = tips['tip']/tips['total_bill']
print(tips[:6])
--------------------------------------------------------------------------------
total_bill tip smoker day time size tip_pct
0 16.99 1.01 No Sun Dinner 2 0.059447
1 10.34 1.66 No Sun Dinner 3 0.160542
2 21.01 3.50 No Sun Dinner 3 0.166587
3 23.68 3.31 No Sun Dinner 2 0.139780
4 24.59 3.61 No Sun Dinner 4 0.146808
5 25.29 4.71 No Sun Dinner 4 0.186240
对Series或DataFrame所有列进行聚合就是使用aggregate和所需函数,或者是调用像mean或std这种方法的。然而,你可能想根据各列同时使用多个函数进行聚合。这是可以做到的。首先,得将根据day和smoker来对tips进行分组:
grouped = tips.groupby(['day','smoker'])
grouped_pct = grouped['tip_pct']
a = grouped_pct.agg('mean')
print(a)
------------------------------------------------------------------------
day smoker
Fri No 0.151650
Yes 0.174783
Sat No 0.158048
Yes 0.147906
Sun No 0.160113
Yes 0.187250
Thur No 0.160298
Yes 0.163863
如果你传递的是函数或函数名的列表,你会获得一个列名是这些函数名的DataFrame:
a = grouped_pct.agg(['mean', 'std', peak_to_peak])
print(a)
-------------------------------------------------------------------------
mean std peak_to_peak
day smoker
Fri No 0.151650 0.028123 0.067349
Yes 0.174783 0.051293 0.159925
Sat No 0.158048 0.039767 0.235193
Yes 0.147906 0.061375 0.290095
Sun No 0.160113 0.042347 0.193226
Yes 0.187250 0.154134 0.644685
Thur No 0.160298 0.038774 0.193350
Yes 0.163863 0.039389 0.151240
这里我们传递了聚合函数的列表agg方法,这些函数会各自运用于数据分组。
如果你传递的是(name,function)元组的列表,每个元组的第一个元素将作为DataFrame的列名:
a = grouped_pct.agg([('foo','mean'),('bar',np.std)])
print(a)
-------------------------------------------------------------------
foo bar
day smoker
Fri No 0.151650 0.028123
Yes 0.174783 0.051293
Sat No 0.158048 0.039767
Yes 0.147906 0.061375
Sun No 0.160113 0.042347
Yes 0.187250 0.154134
Thur No 0.160298 0.038774
Yes 0.163863 0.039389
在DataFrame中,你有更多的选项,你可以指定应用到所有列上的函数列表或每一列上要应用的不同函数。假设我们想要计算tip_pct和total_bill列的三个相同的统计值:
functions = ['count','max','min']
result = grouped['tip_pct','total_bill'].agg(functions)
print(result)
-----------------------------------------------------------------
tip_pct total_bill
count max min count max min
day smoker
Fri No 4 0.187735 0.120385 4 22.75 12.46
Yes 15 0.263480 0.103555 15 40.17 5.75
Sat No 45 0.291990 0.056797 45 48.33 7.25
Yes 42 0.325733 0.035638 42 50.81 3.07
Sun No 57 0.252672 0.059447 57 48.17 8.77
Yes 19 0.710345 0.065660 19 45.35 7.25
Thur No 45 0.266312 0.072961 45 41.19 7.51
Yes 17 0.241255 0.090014 17 43.11 10.34
如你所见,产生的DataFrame拥有分层列,与分别聚合每一列,再以列名作为keys参数使用concat将结果拼接在一起的结果相同:
和之前一样,可以传递具有自定义名称的元组列表:
ftuples = [('Magnum','mean'),('Boost',np.var)]
a = grouped['tip_pct','total_bill'].agg(ftuples)
print(a)
-------------------------------------------------------
tip_pct total_bill
Magnum Boost Magnum Boost
day smoker
Fri No 0.151650 0.000791 18.420000 25.596333
Yes 0.174783 0.002631 16.813333 82.562438
Sat No 0.158048 0.001581 19.661778 79.908965
Yes 0.147906 0.003767 21.276667 101.387535
Sun No 0.160113 0.001793 20.506667 66.099980
Yes 0.187250 0.023757 24.120000 109.046044
Thur No 0.160298 0.001503 17.113111 59.625081
Yes 0.163863 0.001551 19.190588 69.808518
假设你想要将不同的函数应用到一个列或多个列上。要实现这个功能,需要将列名于函数对应关系的字典传递给agg:
a = grouped.agg({'tip':np.max,'size':'sum'})
print(a)
a = grouped.agg({'tip_pct':['min','max','mean','std'],
'size':'sum'})
print(a)
-------------------------------------------------------------------------
tip size
day smoker
Fri No 3.50 9
Yes 4.73 31
Sat No 9.00 115
Yes 10.00 104
Sun No 6.00 167
Yes 6.50 49
Thur No 6.70 112
Yes 5.00 40
tip_pct size
min max mean std sum
day smoker
Fri No 0.120385 0.187735 0.151650 0.028123 9
Yes 0.103555 0.263480 0.174783 0.051293 31
Sat No 0.056797 0.291990 0.158048 0.039767 115
Yes 0.035638 0.325733 0.147906 0.061375 104
Sun No 0.059447 0.252672 0.160113 0.042347 167
Yes 0.065660 0.710345 0.187250 0.154134 49
Thur No 0.072961 0.266312 0.160298 0.038774 112
Yes 0.090014 0.241255 0.163863 0.039389 40
只有多个函数应用于至少一个列时,DataFrame才具有分层列。
2.2 返回不含行索引的聚合数据
在前面所有的列子中,聚合数据返回时都是带有索引的,有时索引是分层的,由唯一的分组键联合形成的,你也可以通过向groupby传递ax_index = False 来禁用分组键作为索引的行为:
a = tips.groupby(['day','smoker']).mean()
print(a)
a = tips.groupby(['day','smoker'],as_index=False).mean()
print(a)
--------------------------------------------------------------------------
total_bill tip size tip_pct
day smoker
Fri No 18.420000 2.812500 2.250000 0.151650
Yes 16.813333 2.714000 2.066667 0.174783
Sat No 19.661778 3.102889 2.555556 0.158048
Yes 21.276667 2.875476 2.476190 0.147906
Sun No 20.506667 3.167895 2.929825 0.160113
Yes 24.120000 3.516842 2.578947 0.187250
Thur No 17.113111 2.673778 2.488889 0.160298
Yes 19.190588 3.030000 2.352941 0.163863
day smoker total_bill tip size tip_pct
0 Fri No 18.420000 2.812500 2.250000 0.151650
1 Fri Yes 16.813333 2.714000 2.066667 0.174783
2 Sat No 19.661778 3.102889 2.555556 0.158048
3 Sat Yes 21.276667 2.875476 2.476190 0.147906
4 Sun No 20.506667 3.167895 2.929825 0.160113
5 Sun Yes 24.120000 3.516842 2.578947 0.187250
6 Thur No 17.113111 2.673778 2.488889 0.160298
7 Thur Yes 19.190588 3.030000 2.352941 0.163863
总结
以上就是今天要讲的内容,本文仅仅简单介绍了利用GroupBy的机制对数据集进行拆分-应用-联合。而这两节分别介绍了拆分时和聚合时可以做到的功能和细则。下一节将围绕应用进行学习。