对数据集进⾏分组并对各组应⽤⼀个函数(⽆论是聚合还是转换),通常是数据分析⼯作中的重要环节。在将数据集加载、融合、准备好之后,通常就是计算分组统计或⽣成透视表。pandas提供了⼀个灵活⾼效的gruopby功能,它使你能以⼀种⾃然的⽅式对数据集进⾏切⽚、切块、摘要等操作。
关系型数据库和SQL(Structured Query Language,结构化查询语⾔)能够如此流⾏的原因之⼀就是其能够⽅便地对数据进⾏连接、过滤、转换和聚合。但是,像SQL这样的查询语⾔所能执⾏的分组运算的种类很有限。在本节中你将会看到,由于Python和pandas强⼤的表达能⼒,我们可以执⾏复杂得多的分组运算(利⽤任何可以接受pandas对象或NumPy数组的函数)。下面会讲到的有:
计算分组摘要统计,如计数、平均值、标准差,或⽤户⾃定义函数。
计算分组的概述统计,⽐如数量、平均值或标准差,或是⽤户定义的函数。
应⽤组内转换或其他运算,如规格化、线性回归、排名或选取⼦集等。
计算透视表或交叉表。
执⾏分位数分析以及其它统计分组分析。
注意:对时间序列数据的聚合(groupby的特殊⽤法之⼀)也称作重采样(resampling),将在
第11篇中单独对其进⾏讲解。
一、GroupBy机制
Hadley Wickham(许多热⻔R语⾔包的作者)创造了⼀个⽤于表示分组运算的术语"split-apply-combine"(拆分-应⽤-合并)。第⼀个阶段,pandas对象(⽆论是Series、DataFrame还是其他的)中的数据会根据你所提供的⼀个或多个键被拆分(split)为多组。拆分操作是在对象的特定轴上执⾏的。例如,DataFrame可以在其⾏(axis=0)或列(axis=1)上进⾏分组。然后,将⼀个函数应⽤(apply)到各个分组并产⽣⼀个新值。最后,所有这些函数的执⾏结果会被合并(combine)到最终的结果对象中。结果对象的形式⼀般取决于数据上所执⾏的操作。
图10-1⼤致说明了⼀个简单的分组聚合过程。
分组键可以有多种形式,且类型不必相同:
列表或数组,其⻓度与待分组的轴⼀样。
表示DataFrame某个列名的值。
字典或Series,给出待分组轴上的值与分组名之间的对应关系。
函数,⽤于处理轴索引或索引中的各个标签。
注意,后三种都只是快捷⽅式⽽已,其最终⽬的仍然是产⽣⼀组⽤于拆分对象的值。如果觉得这些东⻄看起来很抽象,不⽤担⼼,接下来给出⼤量有关于此的示例。⾸先来看看下⾯这个⾮常简单的表格型数据集(以DataFrame的形式):
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)})
df # 输出如下:
key1 key2 data1 data2
0 a one 0.124798 1.680055
1 a two 0.883053 -1.479288
2 b one 1.694490 -0.793570
3 b two 0.046093 -0.179925
4 a one -0.345717 -0.656644
假设你想要按key1进⾏分组,并计算data1列的平均值。实现该功能的⽅式有很多,⽽我们这⾥要⽤的是:访问data1,并根据key1调⽤groupby:
grouped = df['data1'].groupby(df['key1'])
grouped # 输出:<pandas.core.groupby.groupby.SeriesGroupBy object at 0x000001C129676518>
变量grouped是⼀个GroupBy对象。它实际上还没有进⾏任何计算,只是含有⼀些有关分组键df['key1']的中间数据⽽已。换句话说,该对象已经有了接下来对各分组执⾏运算所需的⼀切信息。例如,我们可以调⽤GroupBy的mean⽅法来计算分组平均值:
grouped.mean() # 输出如下:
key1
a 0.220711
b 0.870292
Name: data1, dtype: float64
稍后将详细讲解.mean()的调⽤过程。这⾥最重要的是,数据(Series)根据分组键进⾏了聚合,产⽣了⼀个新的Series,其索引为key1列中的唯⼀值。之所以结果中索引的名称为key1,是因为原始DataFrame的列df['key1']就叫这个名字。
如果我们⼀次传⼊多个数组的列表,就会得到不同的结果:
means = df['data1'].groupby([df['key1'], df['key2']]).mean() # 注意参数是列表
means # 输出如下:
key1 key2
a one -0.110459
two 0.883053
b one 1.694490
two 0.046093
Name: data1, dtype: float64
这⾥,我通过两个键对数据进⾏了分组,得到的Series具有⼀个层次化索引(由唯⼀的键对组成):
means.unstack() # 输出如下:
key2 one two
key1
a -0.110459 0.883053
b 1.694490 0.046093
在这个例⼦中,分组键均为Series。实际上,分组键可以是任何⻓度适当的数组:
states = np.array(['Ohio', 'California', 'California', 'Ohio', 'Ohio'])
years = np.array([2005, 2005, 2006, 2005, 2006])
df['data1'].groupby([states, years]).mean() # 输出如下:
California 2005 0.883053
2006 1.694490
Ohio 2005 0.085446
2006 -0.345717
Name: data1, dtype: float64
通常,分组信息就位于相同的要处理DataFrame中。这⾥,你还可以将列名(可以是字符串、数字或其他Python对象)⽤作分组键:
df.groupby('key1').mean() # 输出如下:注意没有key2列
data1 data2
key1
a 0.220711 -0.151959
b 0.870292 -0.486748
df.groupby(['key1', 'key2']).mean() # 输出如下:
data1 data2
key1 key2
a one -0.110459 0.511705
two 0.883053 -1.479288
b one 1.694490 -0.793570
two 0.046093 -0.179925
你可能已经注意到了,第⼀个例⼦在执⾏df.groupby('key1').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
注意,任何分组关键词中的缺失值,都会被从结果中除去。
1、对分组进⾏迭代
GroupBy对象⽀持迭代,可以产⽣⼀组⼆元元组(由分组名和数据块组成)。看下⾯的例⼦:
for name, group in df.groupby('key1'):
print(name)
print(group)
输出如下:
a
key1 key2 data1 data2
0 a one 0.124798 1.680055
1 a two 0.883053 -1.479288
4 a one -0.345717 -0.656644
b
key1 key2 data1 data2
2 b one 1.694490 -0.793570
3 b two 0.046093 -0.179925
对于多重键的情况,元组的第⼀个元素将会是由键值组成的元组:
for (k1, k2), group, in df.groupby(['key1', 'key2']):
print((k1, k2))
print(group)
输出如下:
('a', 'one')
key1 key2 data1 data2
0 a one 0.124798 1.680055
4 a one -0.345717 -0.656644
('a', 'two')
key1 key2 data1 data2
1 a two 0.883053 -1.479288
('b', 'one')
key1 key2 data1 data2
2 b one 1.69449 -0.79357
('b', 'two')
key1 key2 data1 data2
3 b two 0.046093 -0.179925
当然,你可以对这些数据⽚段做任何操作。有⼀个你可能会觉得有⽤的运算:将这些数据⽚段做成⼀个字典:
pieces = dict(list(df.groupby('key1'))) # 输出如下:
pieces['b']
key1 key2 data1 data2
2 b one 1.694490 -0.793570
3 b two 0.046093 -0.179925
groupby默认是在axis=0上进⾏分组的,通过设置也可以在其他任何轴上进⾏分组。拿上⾯例⼦中的df来说,我们可以根据dtype对列进⾏分组:
df.dtypes # 输出如下:
key1 object
key2 object
data1 float64
data2 float64
dtype: object
grouped = df.groupby(df.dtypes, axis=1)
可以如下打印分组:
for dtype, group in grouped:
print(dtype)
print(group)
输出如下:
float64
data1 data2
0 0.124798 1.680055
1 0.883053 -1.479288
2 1.694490 -0.793570
3 0.046093 -0.179925
4 -0.345717 -0.656644
object
key1 key2
0 a one
1 a two
2 b one
3 b two
4 a one
2、选取⼀列或列的⼦集
对于由DataFrame产⽣的GroupBy对象,如果⽤⼀个(单个字符串)或⼀组(字符串数组)列名对其进⾏索引,就能实现选取部分列进⾏聚合的⽬的。也就是说:
df.groupby('key1')['data1']
df.groupby('key1')[['data2']]
是以下代码的语法糖:
df['data1'].groupby(df['key1'])
df[['data2']].groupby(df['key1'])
尤其对于⼤数据集,很可能只需要对部分列进⾏聚合。例如,在前⾯那个数据集中,如果只需计算data2列的平均值并以DataFrame形式得到结果,可以这样写:
df.groupby(['key1', 'key2'])[['data2']].mean() # 输出如下:
data2
key1 key2
a one 0.511706
two -1.479288
b one -0.793570
two -0.179925
这种索引操作所返回的对象是⼀个已分组的DataFrame(如果传⼊的是列表或数组)或已分组的Series(如果传⼊的是标量形式的单个列名):
s_grouped = df.groupby(['key1', 'key2'])['data2']
s_grouped # 输出:<pandas.core.groupby.groupby.SeriesGroupBy object at 0x000001925FE1ABE0>
s_grouped.mean() # 输出如下:
key1 key2
a one 0.511706
two -1.479288
b one -0.793570
two -0.179925
Name: data2, dtype: float64
s_grouped.size() # 统计分组数量,输出如下:
key1 key2
a one 2
two 1
b one 1
two 1
Name: data2, dtype: int64
3、通过字典或Series进⾏分组
除数组以外,分组信息还可以其他形式存在。来看另⼀个示例DataFrame:
people = pd.DataFrame(np.random.randn(5, 5),
columns=['a', 'b', 'c', 'd', 'e'],
index=['Joe', 'Steve', 'Wes', 'Jim', 'Travis'])
people.iloc[2:3, [1, 2]] = np.nan # Add a few NA values
people # 输出如下:
a b c d e
Joe -1.301882 -0.547029 -0.154675 -1.503791 -1.756021
Steve 0.049068 0.564180 0.417553 0.795936 0.990268
Wes 1.745869 NaN NaN -0.978691 0.449792
Jim -0.699714 -0.530137 0.261226 0.083186 -0.645877
Travis -0.901562 -2.880716 -0.685193 0.321203 0.832955
现在,假设已知列的分组关系,并希望根据分组计算列的和:
mapping = {'a': 'red', 'b': 'red', 'c': 'blue',
'd': 'blue', 'e': 'red', 'f' : 'orange'}
现在,你可以将这个字典传给groupby,来构造数组,但我们可以直接传递字典(我包含了键“f”来强调,存在未使⽤的分组键是可以的):
by_column = people.groupby(mapping, axis=1)
by_column.sum() # 输出如下:
blue red
Joe -1.658466 -3.604932
Steve 1.213489 1.603516
Wes -0.978691 2.195661
Jim 0.344412 -1.875728
Travis -0.363991 -2.949323
Series也有同样的功能,它可以被看做⼀个固定⼤⼩的映射:
map_series = pd.Series(mapping)
map_series # 输出如下:
a red
b red
c blue
d blue
e red
f orange
dtype: object
people.groupby(map_series, axis=1).count() # 输出如下:
blue red
Joe 2 3
Steve 2 3
Wes 1 2
Jim 2 3
Travis 2 3
4、通过函数进⾏分组
⽐起使⽤字典或Series,使⽤Python函数是⼀种更原⽣的⽅法定义分组映射。任何被当做分组键的函数都会在各个索引值上被调⽤⼀次,其返回值就会被⽤作分组名称。具体点说,以上⼀⼩节的示例DataFrame为例,其索引值为⼈的名字。你可以计算⼀个字符串⻓度的数组,更简单的⽅法是传⼊len函数:
people.groupby(len).sum() # 按行索引的字符长度进行分组求和,输出如下:
a b c d e
3 -0.255727 -1.077167 0.106551 -2.399296 -1.952106
5 0.049068 0.564180 0.417553 0.795936 0.990268
6 -0.901562 -2.880716 -0.685193 0.321203 0.832955
将函数跟数组、列表、字典、Series混合使⽤也不是问题,因为任何东⻄在内部都会被转换为数组:
key_list = ['one', 'one', 'one', 'two', 'two']
people.groupby([len, key_list]).min() # 输出如下:
a b c d e
3 one -1.301882 -0.547029 -0.154675 -1.503791 -1.756021
two -0.699714 -0.530137 0.261226 0.083186 -0.645877
5 one 0.049068 0.564180 0.417553 0.795936 0.990268
6 two -0.901562 -2.880716 -0.685193 0.321203 0.832955
5、根据索引级别分组
层次化索引数据集最⽅便的地⽅就在于它能够根据轴索引的⼀个级别进⾏聚合:
columns = pd.MultiIndex.from_arrays([['US', 'US', 'US', 'JP', 'JP'],
[1, 3, 5, 1, 3]],
names=['cty', 'tenor'])
hier_df = pd.DataFrame(np.random.randn(4, 5), columns=columns)
hier_df # 输出如下:
cty US JP
tenor 1 3 5 1 3
0 -0.875317 -1.027136 -0.263697 0.535952 -1.237080
1 -0.042287 1.111177 0.476042 1.412364 -0.238579
2 -0.419416 0.736647 0.701512 -0.876155 -0.754143
3 1.046427 -1.283016 -0.565056 -0.671289 -0.601971
要根据级别分组,使⽤level关键字传递级别序号或名字:
hier_df.groupby(level='cty', axis=1).count() # 输出如下:
cty JP US
0 2 3
1 2 3
2 2 3
3 2 3
二、数据聚合
聚合指的是任何能够从数组产⽣标量值的数据转换过程。之前的例⼦已经⽤过⼀些,⽐如mean、count、min以及sum等。你可能想知道在GroupBy对象上调⽤mean()时究竟发⽣了什么。许多常⻅的聚合运算(如表10-1所示)都有进⾏优化。然⽽,除了这些⽅法,你还可以使⽤其它的。
你可以使⽤⾃⼰发明的聚合运算,还可以调⽤分组对象上已经定义好的任何⽅法。例如,quantile可以计算Series或DataFrame列的样本分位数。
虽然quantile并没有明确地实现于GroupBy,但它是⼀个Series⽅法,所以这⾥是能⽤的。实际上,GroupBy会⾼效地对Series进⾏切⽚,然后对各⽚调⽤piece.quantile(0.9),最后将这些结果组装成最终结果:
df # 有df变量数据如下:
key1 key2 data1 data2
0 a one 0.124798 1.680055
1 a two 0.883053 -1.479288
2 b one 1.694490 -0.793570
3 b two 0.046093 -0.179925
4 a one -0.345717 -0.656644
grouped = df.groupby('key1')
grouped['data1'].quantile(0.9) # 输出如下
key1
a 0.731402
b 1.529650
Name: data1, dtype: float64
如果要使⽤你⾃⼰的聚合函数,只需将其传⼊aggregate或agg⽅法即可:
def peak_to_peak(arr):
return arr.max() - arr.min()
grouped.agg(peak_to_peak) # 输出如下:
data1 data2
key1
a 1.228770 3.159343
b 1.648397 0.613645
你可能已注意到,有些⽅法(如describe)也是可以⽤在这⾥的,即使严格来讲,它们并⾮聚合运算:
grouped.describe() # 输出如下:
data1
count mean std min 25% 50% 75%
key1
a 3.0 0.220711 0.619975 -0.345717 -0.110459 0.124798 0.503926
b 2.0 0.870291 1.165593 0.046093 0.458192 0.870291 1.282391
data2
max count mean std min 25% 50%
key1
a 0.883053 3.0 -0.151959 1.639022 -1.479288 -1.067966 -0.656644
b 1.694490 2.0 -0.486747 0.433913 -0.793570 -0.640159 -0.486747
75% max
key1
a 0.511706 1.680055
b -0.333336 -0.179925
在后⾯的第三节,将详细说明这到底是怎么回事。
注意:⾃定义聚合函数要⽐表10-1中那些经过优化的函数慢得多。这是因为在构造中间分组数据块时存在⾮常⼤的开销(函数调⽤、数据重排等)。
2、⾯向列的多函数应⽤
回到前⾯⼩费的例⼦。使⽤read_csv导⼊数据之后,我们添加了⼀个⼩费百分⽐的列tip_pct:
tips = pd.read_csv('examples/tips.csv')
# Add tip percentage of total bill,添加小费百分比
tips['tip_pct'] = tips['tip'] / tips['total_bill']
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'])
注意,对于表10-1中的那些描述统计,可以将函数名以字符串的形式传⼊:
grouped_pct = grouped['tip_pct']
grouped_pct.agg('mean') # 输出如下:函数名以字符串形式传入
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
Name: tip_pct, dtype: float64
如果传⼊⼀组函数或函数名,得到的DataFrame的列就会以相应的函数命名:
grouped_pct.agg(['mean', 'std', peak_to_peak]) # 输出如下:
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
这⾥,我们传递了⼀组聚合函数进⾏聚合,独⽴对数据分组进⾏评估。
你并⾮⼀定要接受GroupBy⾃动给出的那些列名,特别是lambda函数,它们的名称是'<lambda>',这样的辨识度就很低了(通过函数的name属性看看就知道了)。因此,如果传⼊的是⼀个由(name,function)元组组成的列表,则各元组的第⼀个元素就会被⽤作DataFrame的列名(可以将这种⼆元元组列表看做⼀个有序映射):
grouped_pct.agg([('foo', 'mean'), ('bar', np.std)]) # 输出如下:
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', 'mean', 'max']
result = grouped['tip_pct', 'total_bill'].agg(functions)
result # 输出如下:
tip_pct total_bill
count mean max count mean max
day smoker
Fri No 4 0.151650 0.187735 4 18.420000 22.75
Yes 15 0.174783 0.263480 15 16.813333 40.17
Sat No 45 0.158048 0.291990 45 19.661778 48.33
Yes 42 0.147906 0.325733 42 21.276667 50.81
Sun No 57 0.160113 0.252672 57 20.506667 48.17
Yes 19 0.187250 0.710345 19 24.120000 45.35
Thur No 45 0.160298 0.266312 45 17.113111 41.19
Yes 17 0.163863 0.241255 17 19.190588 43.11
如你所⻅,结果DataFrame拥有层次化的列,这相当于分别对各列进⾏聚合,然后⽤concat将结果组装到⼀起,使⽤列名⽤作keys参数:
result['tip_pct'] # 输出如下:
count mean max
day smoker
Fri No 4 0.151650 0.187735
Yes 15 0.174783 0.263480
Sat No 45 0.158048 0.291990
Yes 42 0.147906 0.325733
Sun No 57 0.160113 0.252672
Yes 19 0.187250 0.710345
Thur No 45 0.160298 0.266312
Yes 17 0.163863 0.241255
跟前⾯⼀样,这⾥也可以传⼊带有⾃定义名称的⼀组元组:
ftuples = [('Durchschnitt', 'mean'), ('Abweichung', np.var)]
grouped['tip_pct', 'total_bill'].agg(ftuples) # 输出如下:
tip_pct total_bill
Durchschnitt Abweichung Durchschnitt Abweichung
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传⼊⼀个从列名映射到函数的字典:
grouped.agg({'tip' : np.max, 'size' : 'sum'}) # 输出如下:
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
grouped.agg({'tip_pct' : ['min', 'max', 'mean', 'std'], 'size' : 'sum'}) # 输出如下:
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、以“没有⾏索引”的形式返回聚合数据
到⽬前为⽌,所有示例中的聚合数据都有由唯⼀的分组键组成的索引(可能还是层次化的)。由于并不总是需要如此,所以你可以向groupby传⼊as_index=False以禁⽤该功能:
tips.groupby(['day', 'smoker'], as_index=False).mean() # 输出如下:
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
当然,对结果调⽤reset_index也能得到这种形式的结果。使⽤as_index=False⽅法可以避免⼀些不必要的计算。
三、apply:⼀般性的“拆分-应⽤-合并”
最通⽤的GroupBy⽅法是apply,本节剩余部分将重点讲解它。如图10-2所示,apply会将待处理的对象拆分成多个⽚段,然后对各⽚段调⽤传⼊的函数,最后尝试将各⽚段组合到⼀起。
图10-2 分组聚合示例
回到之前那个⼩费数据集,假设你想要根据分组选出最⾼的5个tip_pct值。⾸先,编写⼀个选取指定列具有最⼤值的⾏的函数:
def top(df, n=5, column='tip_pct'):
return df.sort_values(by=column)[-n:] # 选取方法是:先升充排序,最后5行
top(tips, n=6) # 调用函数,输出如下:
total_bill tip smoker day time size tip_pct
109 14.31 4.00 Yes Sat Dinner 2 0.279525
183 23.17 6.50 Yes Sun Dinner 4 0.280535
232 11.61 3.39 No Sat Dinner 2 0.291990
67 3.07 1.00 Yes Sat Dinner 1 0.325733
178 9.60 4.00 Yes Sun Dinner 2 0.416667
172 7.25 5.15 Yes Sun Dinner 2 0.710345
现在,如果对smoker分组并⽤该函数调⽤apply,就会得到:
tips.groupby('smoker').apply(top) # 输出如下:
total_bill tip smoker day time size tip_pct
smoker
No 88 24.71 5.85 No Thur Lunch 2 0.236746
185 20.69 5.00 No Sun Dinner 5 0.241663
51 10.29 2.60 No Sun Dinner 2 0.252672
149 7.51 2.00 No Thur Lunch 2 0.266312
232 11.61 3.39 No Sat Dinner 2 0.291990
Yes 109 14.31 4.00 Yes Sat Dinner 2 0.279525
183 23.17 6.50 Yes Sun Dinner 4 0.280535
67 3.07 1.00 Yes Sat Dinner 1 0.325733
178 9.60 4.00 Yes Sun Dinner 2 0.416667
172 7.25 5.15 Yes Sun Dinner 2 0.710345
这⾥发⽣了什么?top函数在DataFrame的各个⽚段上调⽤,然后结果由pandas.concat组装到⼀起,并以分组名称进⾏了标记。于是,最终结果就有了⼀个层次化索引,其内层索引值来⾃原DataFrame。
如果传给apply的函数能够接受其他参数或关键字,则可以将这些内容放在函数名后⾯⼀并传⼊:
tips.groupby(['smoker', 'day']).apply(top, n=1, column='total_bill') # 输出如下:
total_bill tip smoker day time size tip_pct
smoker day
No Fri 94 22.75 3.25 No Fri Dinner 2 0.142857
Sat 212 48.33 9.00 No Sat Dinner 4 0.186220
Sun 156 48.17 5.00 No Sun Dinner 6 0.103799
Thur 142 41.19 5.00 No Thur Lunch 5 0.121389
Yes Fri 95 40.17 4.73 Yes Fri Dinner 4 0.117750
Sat 170 50.81 10.00 Yes Sat Dinner 3 0.196812
Sun 182 45.35 3.50 Yes Sun Dinner 3 0.077178
Thur 197 43.11 5.00 Yes Thur Lunch 4 0.115982
注意:除这些基本⽤法之外,能否充分发挥apply的威⼒很⼤程度上取决于你的创造⼒。传⼊的那个函数能做什么全由你说了算,它只需返回⼀个pandas对象或标量值即可。本章后续部分的示例主要⽤于讲解如何利⽤groupby解决各种各样的问题。
可能你已经想起来了,之前我在GroupBy对象上调⽤过describe:
result = tips.groupby('smoker')['tip_pct'].describe()
result # 输出如下:
count mean std min 25% 50% 75%
smoker
No 151.0 0.159328 0.039910 0.056797 0.136906 0.155625 0.185014
Yes 93.0 0.163196 0.085119 0.035638 0.106771 0.153846 0.195059
max
smoker
No 0.291990
Yes 0.710345
result.unstack('smoker') # 输出如下:
smoker
count No 151.000000
Yes 93.000000
mean No 0.159328
Yes 0.163196
std No 0.039910
Yes 0.085119
min No 0.056797
Yes 0.035638
25% No 0.136906
Yes 0.106771
50% No 0.155625
Yes 0.153846
75% No 0.185014
Yes 0.195059
max No 0.291990
Yes 0.710345
dtype: float64
在GroupBy中,当你调⽤诸如describe之类的⽅法时,实际上只是应⽤了下⾯两条代码的快捷⽅式⽽已:
f = lambda x: x.describe()
grouped.apply(f)
1、禁⽌分组键
从上⾯的例⼦中可以看出,分组键会跟原始对象的索引共同构成结果对象中的层次化索引。将group_keys=False传⼊groupby即可禁⽌该效果:
tips.groupby('smoker', group_keys=False).apply(top) # 输出如下:
total_bill tip smoker day time size tip_pct
88 24.71 5.85 No Thur Lunch 2 0.236746
185 20.69 5.00 No Sun Dinner 5 0.241663
51 10.29 2.60 No Sun Dinner 2 0.252672
149 7.51 2.00 No Thur Lunch 2 0.266312
232 11.61 3.39 No Sat Dinner 2 0.291990
109 14.31 4.00 Yes Sat Dinner 2 0.279525
183 23.17 6.50 Yes Sun Dinner 4 0.280535
67 3.07 1.00 Yes Sat Dinner 1 0.325733
178 9.60 4.00 Yes Sun Dinner 2 0.416667
172 7.25 5.15 Yes Sun Dinner 2 0.710345
2、分位数和桶分析
在第8篇中讲过,pandas有⼀些能根据指定⾯元或样本分位数将数据拆分成多块的⼯具(⽐如cut和qcut)。将这些函数跟groupby结合起来,就能⾮常轻松地实现对数据集的桶(bucket)或分位数(quantile)分析了。以下⾯这个简单的随机数据集为例,我们利⽤cut将其装⼊⻓度相等的桶中:
frame = pd.DataFrame({'data1': np.random.randn(1000),
'data2': np.random.randn(1000)})
quartiles = pd.cut(frame.data1, 4) # 把data1列平均切分成4个区段,长度相等
quartiles[:10] # 输出如下:(每个数字对应的区段信息)
0 (-1.606, -0.0295]
1 (-1.606, -0.0295]
2 (-3.19, -1.606]
3 (-1.606, -0.0295]
4 (-1.606, -0.0295]
5 (-0.0295, 1.547]
6 (-1.606, -0.0295]
7 (-0.0295, 1.547]
8 (-1.606, -0.0295]
9 (-0.0295, 1.547]
Name: data1, dtype: category
Categories (4, interval[float64]): [(-3.19, -1.606] < (-1.606, -0.0295] < (-0.0295, 1.547] < (1.547, 3.124]]
由cut返回的Categorical(分类)对象可直接传递到groupby。因此,我们可以像下⾯这样对data2列做⼀些统计计算:
def get_stats(group):
return {'min': group.min(), 'max': group.max(),
'count': group.count(), 'mean': group.mean()}
grouped = frame.data2.groupby(quartiles) # 使用data1列的分类对data2列进行分组统计
grouped.apply(get_stats).unstack() # 输出如下:
count max mean min
data1
(-3.19, -1.606] 61.0 2.159674 0.058952 -2.327527
(-1.606, -0.0295] 446.0 2.870851 -0.054205 -2.418780
(-0.0295, 1.547] 425.0 3.155538 0.048552 -2.949279
(1.547, 3.124] 68.0 3.915751 0.093420 -2.354376
这些都是⻓度相等的桶。要根据样本分位数得到⼤⼩相等的桶,使⽤qcut即可。传⼊labels=False即可只获取分位数的编号:
# Return quantile numbers,获取分组编号
grouping = pd.qcut(frame.data1, 10, labels=False) # 对data1列切分为10段大小相等的桶
grouped = frame.data2.groupby(grouping)
grouped.apply(get_stats).unstack() # 输出如下:
count max mean min
data1
0 100.0 2.159674 0.053973 -2.327527
1 100.0 2.476054 -0.099008 -1.854281
2 100.0 2.870851 0.124314 -2.394580
3 100.0 2.254675 0.021759 -2.343616
4 100.0 2.356002 -0.293433 -2.418780
5 100.0 2.340526 0.070588 -2.949279
6 100.0 2.746106 0.023422 -2.748830
7 100.0 2.423997 -0.009665 -2.427240
8 100.0 3.155538 0.067272 -2.061481
9 100.0 3.915751 0.104857 -2.681458
在第12篇详细讲解pandas的Categorical类型。
3、示例:⽤特定于分组的值填充缺失值
对于缺失数据的清理⼯作,有时你会⽤dropna将其替换掉,⽽有时则可能会希望⽤⼀个固定值或由数据集本身所衍⽣出来的值去填充NA值。这时就得使⽤fillna这个⼯具了。在下⾯这个例⼦中,我⽤平均值去填充NA值:
s = pd.Series(np.random.randn(6))
s[::2] = np.nan
s # 输出如下:
0 NaN
1 -1.160118
2 NaN
3 -2.292183
4 NaN
5 0.918753
dtype: float64
s.fillna(s.mean()) # 输出如下:平均值填充
0 -0.844516
1 -1.160118
2 -0.844516
3 -2.292183
4 -0.844516
5 0.918753
dtype: float64
假设你需要对不同的分组填充不同的值。⼀种⽅法是将数据分组,并使⽤apply和⼀个能够对各数据块调⽤fillna的函数即可。下⾯是⼀些有关⼏个州的示例数据,这些州⼜被分为东部和⻄部:
states = ['Ohio', 'New York', 'Vermont', 'Florida',
'Oregon', 'Nevada', 'California', 'Idaho']
group_key = ['East'] * 4 + ['West'] * 4
data = pd.Series(np.random.randn(8), index=states)
data # 输出如下:
Ohio 2.050564
New York 0.801299
Vermont -0.239287
Florida 2.218410
Oregon -0.793883
Nevada 0.654287
California -1.627796
Idaho 0.280564
dtype: float64
['East'] * 4产⽣了⼀个列表,包括了['East']中元素的四个拷⻉。将这些列表串联起来。
将⼀些值设为缺失:
data[['Vermont', 'Nevada', 'Idaho']] = np.nan
data # 输出如下:
Ohio 2.050564
New York 0.801299
Vermont NaN
Florida 2.218410
Oregon -0.793883
Nevada NaN
California -1.627796
Idaho NaN
dtype: float64
data.groupby(group_key).mean() # 输出如下:注意group_key的长度需要与data的index长度一致
East 1.690091
West -1.210839
dtype: float64
我们可以⽤分组平均值去填充NA值:
fill_mean = lambda g: g.fillna(g.mean()) # 把fillna()方法包装进函数中,方便使用apply()方法
data.groupby(group_key).apply(fill_mean) # 输出如下:
Ohio 2.050564
New York 0.801299
Vermont 1.690091
Florida 2.218410
Oregon -0.793883
Nevada -1.210839
California -1.627796
Idaho -1.210839
dtype: float64
另外,也可以在代码中预定义各组的填充值。由于分组具有⼀个name属性,所以我们可以拿来⽤⼀下:
fill_values = {'East': 0.5, 'West': -1} # 预定义填充值
fill_func = lambda g: g.fillna(fill_values[g.name]) # 使用预定义值填充
data.groupby(group_key).apply(fill_func) # 输出如下:
Ohio 2.050564
New York 0.801299
Vermont 0.500000
Florida 2.218410
Oregon -0.793883
Nevada -1.000000
California -1.627796
Idaho -1.000000
dtype: float64
4、示例:随机采样和排列
假设你想要从⼀个⼤数据集中随机抽取(进⾏替换或不替换)样本以进⾏蒙特卡罗模拟(Monte Carlo simulation)或其他分析⼯作。“抽取”的⽅式有很多,这⾥使⽤的⽅法是对Series使⽤sample⽅法:
# Hearts, Spades, Clubs, Diamonds,模拟扑克牌
suits = ['H', 'S', 'C', 'D']
card_val = (list(range(1, 11)) + [10] * 3) * 4
base_names = ['A'] + list(range(2, 11)) + ['J', 'K', 'Q']
cards = []
for suit in ['H', 'S', 'C', 'D']:
cards.extend(str(num) + suit for num in base_names)
deck = pd.Series(card_val, index=cards)
现在我有了⼀个⻓度为52的Series,其索引包括牌名,值则是21点或其他游戏中⽤于计分的点数(为了简单起⻅,我当A的点数为1):
deck[:13] # 输出如下:
AH 1
2H 2
3H 3
4H 4
5H 5
6H 6
7H 7
8H 8
9H 9
10H 10
JH 10
KH 10
QH 10
dtype: int64
现在,根据我上⾯所讲的,从整副牌中抽出5张,代码如下:
def draw(deck, n=5): # 定义一个函数实现
return deck.sample(n)
draw(deck) # 输出如下:
9C 9
4S 4
6S 6
5S 5
10C 10
dtype: int64
假设你想要从每种花⾊中随机抽取两张牌。由于花⾊是牌名的最后⼀个字符,所以我们可以据此进⾏分组,并使⽤apply:
get_suit = lambda card: card[-1] # last letter is suit,取最后一个字符
deck.groupby(get_suit).apply(draw, n=2) # 输出如下:对于Series的分组默认是index,get_suit函数参数card对应deck的index
C 3C 3
2C 2
D 6D 6
QD 10
H 10H 10
KH 10
S JS 10
9S 9
dtype: int64
或者,也可以这样写:
deck.groupby(get_suit, group_keys=False).apply(draw, n=2) # 输出如下:
AC 1
10C 10
2D 2
6D 6
6H 6
4H 4
2S 2
6S 6
dtype: int64
5、示例:分组加权平均数和相关系数
根据groupby的“拆分-应⽤-合并”范式,可以进⾏DataFrame的列与列之间或两个Series之间的运算(⽐如分组加权平均)。以下⾯这个数据集为例,它含有分组键、值以及⼀些权重值:
df = pd.DataFrame({'category': ['a', 'a', 'a', 'a',
'b', 'b', 'b', 'b'],
'data': np.random.randn(8),
'weights': np.random.rand(8)})
df # 输出如下:
category data weights
0 a -0.041007 0.016285
1 a 0.127990 0.611796
2 a -0.113209 0.442395
3 a -1.541167 0.881116
4 b 1.459021 0.053399
5 b 0.083923 0.633871
6 b 0.993399 0.461408
7 b 0.055818 0.907583
然后可以利⽤category计算分组加权平均数:
grouped = df.groupby('category') # 分组
get_wavg = lambda g: np.average(g['data'], weights=g['weights']) # 加权平均数
grouped.apply(get_wavg) # 输出如下:(分组加权平均数)
category
a -0.681697
b 0.311307
dtype: float64
另⼀个例⼦,考虑⼀个来⾃Yahoo!Finance的数据集,其中含有⼏只股票和标准普尔500指数(符号SPX)的收盘价:
close_px = pd.read_csv('examples/stock_px_2.csv', parse_dates=True,
index_col=0) # index_col=0禁止自动行索引
close_px.info() # 输出如下:
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 2214 entries, 2003-01-02 to 2011-10-14
Data columns (total 4 columns):
AAPL 2214 non-null float64
MSFT 2214 non-null float64
XOM 2214 non-null float64
SPX 2214 non-null float64
dtypes: float64(4)
memory usage: 86.5 KB
close_px[-4:] # 输出如下:
AAPL MSFT XOM SPX
2011-10-11 400.29 27.00 76.27 1195.54
2011-10-12 402.19 26.96 77.16 1207.25
2011-10-13 408.43 27.18 76.37 1203.66
2011-10-14 422.00 27.27 78.11 1224.58
来做⼀个⽐较有趣的任务:计算⼀个由⽇收益率(通过百分数变化计算)与SPX之间的年度相关系数组成的DataFrame。下⾯是⼀个实现办法,我们先创建⼀个函数,⽤它计算每列和SPX列的成对相关系数:
spx_corr = lambda x: x.corrwith(x['SPX']) # 先计算每列与SPX列的成对相关系数
接下来,我们使⽤pct_change计算close_px的百分⽐变化:
rets = close_px.pct_change().dropna()
最后,我们⽤年对百分⽐变化进⾏分组,可以⽤⼀个⼀⾏的函数,从每⾏的标签返回每个datetime标签的year属性:
get_year = lambda x: x.year # 从每行的标签返回year属性
by_year = rets.groupby(get_year) # 对rets结果按year进行分组
by_year.apply(spx_corr) # 输出如下:计算每列与SPX列的相关系数
AAPL MSFT XOM SPX
2003 0.541124 0.745174 0.661265 1.0
2004 0.374283 0.588531 0.557742 1.0
2005 0.467540 0.562374 0.631010 1.0
2006 0.428267 0.406126 0.518514 1.0
2007 0.508118 0.658770 0.786264 1.0
2008 0.681434 0.804626 0.828303 1.0
2009 0.707103 0.654902 0.797921 1.0
2010 0.710105 0.730118 0.839057 1.0
2011 0.691931 0.800996 0.859975 1.0
当然,你还可以计算列与列之间的相关系数。这⾥,我们计算AAPL和MSFT的年相关系数:
by_year.apply(lambda g: g['AAPL'].corr(g['MSFT'])) # 输出如下:
2003 0.480868
2004 0.259024
2005 0.300093
2006 0.161735
2007 0.417738
2008 0.611901
2009 0.432738
2010 0.571946
2011 0.581987
dtype: float64
6、示例:组级别的线性回归
顺着上⼀个例⼦继续,你可以⽤groupby执⾏更为复杂的分组统计分析,只要函数返回的是pandas对象或标量值即可。例如,可以定义下⾯这个regress函数(利⽤statsmodels计量经济学库)对各数据块执⾏普通最⼩⼆乘法(Ordinary LeastSquares,OLS)回归:
import statsmodels.api as sm
def regress(data, yvar, xvars):
Y = data[yvar]
X = data[xvars]
X['intercept'] = 1
result = sm.OLS(Y, X).fit()
return result.params
现在,为了按年计算AAPL对SPX收益率的线性回归,执⾏:
by_year.apply(regress, 'AAPL', ['SPX']) # 输出如下:
SPX intercept
2003 1.195406 0.000710
2004 1.363463 0.004201
2005 1.766415 0.003246
2006 1.645496 0.000080
2007 1.198761 0.003438
2008 0.968016 -0.001110
2009 0.879103 0.002954
2010 1.052608 0.001261
2011 0.806605 0.001514
四、透视表和交叉表
透视表(pivot table)是各种电⼦表格程序和其他数据分析软件中⼀种常⻅的数据汇总⼯具。它根据⼀个或多个键对数据进⾏聚合,并根据⾏和列上的分组键将数据分配到各个矩形区域中。在Python和pandas中,可以通过本章所介绍的groupby功能以及(能够利⽤层次化索引的)重塑运算制作透视表。DataFrame有⼀个pivot_table⽅法,此外还有⼀个顶级的pandas.pivot_table函数。除能为groupby提供便利之外,pivot_table还可以添加分项⼩计,也叫做margins。
回到⼩费数据集,假设我想要根据day和smoker计算分组平均数(pivot_table的默认聚合类型),并将day和smoker放到⾏上:
tips.pivot_table(index=['day', 'smoker']) # 输出如下:
size tip tip_pct total_bill
day smoker
Fri No 2.250000 2.812500 0.151650 18.420000
Yes 2.066667 2.714000 0.174783 16.813333
Sat No 2.555556 3.102889 0.158048 19.661778
Yes 2.476190 2.875476 0.147906 21.276667
Sun No 2.929825 3.167895 0.160113 20.506667
Yes 2.578947 3.516842 0.187250 24.120000
Thur No 2.488889 2.673778 0.160298 17.113111
Yes 2.352941 3.030000 0.163863 19.190588
可以⽤groupby直接来做。现在,假设我们只想聚合tip_pct和size,⽽且想根据time进⾏分组。我将smoker放到列上,把day放到⾏上:
tips.pivot_table(['tip_pct', 'size'], index=['time', 'day'], columns='smoker') # 输出如下:
size tip_pct
smoker No Yes No Yes
time day
Dinner Fri 2.000000 2.222222 0.139622 0.165347
Sat 2.555556 2.476190 0.158048 0.147906
Sun 2.929825 2.578947 0.160113 0.187250
Thur 2.000000 NaN 0.159744 NaN
Lunch Fri 3.000000 1.833333 0.187735 0.188937
Thur 2.500000 2.352941 0.160311 0.163863
还可以对这个表作进⼀步的处理,传⼊margins=True添加分项⼩计。这将会添加标签为All的⾏和列,其值对应于单个等级中所有数据的分组统计:
tips.pivot_table(['tip_pct', 'size'], index=['time', 'day'],
columns='smoker', margins=True) # 输出如下:
size tip_pct
smoker No Yes All No Yes All
time day
Dinner Fri 2.000000 2.222222 2.166667 0.139622 0.165347 0.158916
Sat 2.555556 2.476190 2.517241 0.158048 0.147906 0.153152
Sun 2.929825 2.578947 2.842105 0.160113 0.187250 0.166897
Thur 2.000000 NaN 2.000000 0.159744 NaN 0.159744
Lunch Fri 3.000000 1.833333 2.000000 0.187735 0.188937 0.188765
Thur 2.500000 2.352941 2.459016 0.160311 0.163863 0.161301
All 2.668874 2.408602 2.569672 0.159328 0.163196 0.160803
这⾥,All值为平均数:不单独考虑烟⺠与⾮烟⺠(All列),不单独考虑⾏分组两个级别中的任何单项(All⾏)。
要使⽤其他的聚合函数,将其传给aggfunc即可。例如,使⽤count或len可以得到有关分组⼤⼩的
交叉表(计数或频率):
tips.pivot_table('tip_pct', index=['time', 'smoker'], columns='day',
aggfunc=len, margins=True) # 输出如下:
day Fri Sat Sun Thur All
time smoker
Dinner No 3.0 45.0 57.0 1.0 106.0
Yes 9.0 42.0 19.0 NaN 70.0
Lunch No 1.0 NaN NaN 44.0 45.0
Yes 6.0 NaN NaN 17.0 23.0
All 19.0 87.0 76.0 62.0 244.0
如果存在空的组合(也就是NA),你可能会希望设置⼀个fill_value:
tips.pivot_table('tip_pct', index=['time', 'size', 'smoker'],
columns='day', aggfunc='mean', fill_value=0) # 输出如下:
day Fri Sat Sun Thur
time size smoker
Dinner 1 No 0.000000 0.137931 0.000000 0.000000
Yes 0.000000 0.325733 0.000000 0.000000
2 No 0.139622 0.162705 0.168859 0.159744
Yes 0.171297 0.148668 0.207893 0.000000
3 No 0.000000 0.154661 0.152663 0.000000
Yes 0.000000 0.144995 0.152660 0.000000
4 No 0.000000 0.150096 0.148143 0.000000
Yes 0.117750 0.124515 0.193370 0.000000
5 No 0.000000 0.000000 0.206928 0.000000
Yes 0.000000 0.106572 0.065660 0.000000
6 No 0.000000 0.000000 0.103799 0.000000
Lunch 1 No 0.000000 0.000000 0.000000 0.181728
Yes 0.223776 0.000000 0.000000 0.000000
2 No 0.000000 0.000000 0.000000 0.166005
Yes 0.181969 0.000000 0.000000 0.158843
3 No 0.187735 0.000000 0.000000 0.084246
Yes 0.000000 0.000000 0.000000 0.204952
4 No 0.000000 0.000000 0.000000 0.138919
Yes 0.000000 0.000000 0.000000 0.155410
5 No 0.000000 0.000000 0.000000 0.121389
6 No 0.000000 0.000000 0.000000 0.173706
pivot_table的参数说明请参⻅表10-2。
表10-2 pivot_table的选项
函数名 说明
values 待聚合的列的名称,默认聚合所有数值列
index 用于分组的列名或其他分组键,出现在结果透视表的行
columns 用于分组的列名或其他分组键,出现在结果透视表的列
aggfunc 聚合函数或函数列表,默认为mean。可以是任何对groupby有效的函数
fill_value 用于替换结果表中的缺失值
dropna 如果为True,不添加条目都为NA的列
margins 添加行/列小计和总计,默认为False
1、交叉表:crosstab
交叉表(cross-tabulation,简称crosstab)是⼀种⽤于计算分组频率的特殊透视表。看下⾯的例⼦:
nati = ['USA', 'Japan', 'USA', 'Japan', 'Japan', 'Japan', 'USA', 'USA', 'Japan', 'USA']
handed = ['Right-handed', 'left-handed', 'Right-handed', 'Right-handed', 'left-handed',
'Right-handed', 'Right-handed', 'left-handed', 'Right-handed', 'Right-handed']
data = pd.DataFrame({'Sample': np.arange(1,11), 'Nationality': nati, 'Handedness': handed})
data # 输出如下:
Sample Nationality Handedness
0 1 USA Right-handed
1 2 Japan left-handed
2 3 USA Right-handed
3 4 Japan Right-handed
4 5 Japan left-handed
5 6 Japan Right-handed
6 7 USA Right-handed
7 8 USA left-handed
8 9 Japan Right-handed
9 10 USA Right-handed
作为调查分析的⼀部分,我们可能想要根据国籍和⽤⼿习惯对这段数据进⾏统计汇总。虽然可以⽤pivot_table实现该功能,但是pandas.crosstab函数会更⽅便:
pd.crosstab(data.Nationality, data.Handedness, margins=True) # 输出如下:第一个参数做为行标签,第二个参数做为列标签
Handedness Right-handed left-handed All
Nationality
Japan 3 2 5
USA 4 1 5
All 7 3 10
crosstab的前两个参数可以是数组或Series,或是数组列表。就像⼩费数据:
pd.crosstab([tips.time, tips.day], tips.smoker, margins=True)
smoker No Yes All
time day
Dinner Fri 3 9 12
Sat 45 42 87
Sun 57 19 76
Thur 1 0 1
Lunch Fri 1 6 7
Thur 44 17 61
All 151 93 244
掌握pandas数据分组⼯具既有助于数据清理,也有助于建模或统计分析⼯作。