数据基础---《利用Python进行数据分析·第2版》第10章 数据聚合与分组运算

之前自己对于numpy和pandas是要用的时候东学一点西一点,直到看到《利用Python进行数据分析·第2版》,觉得只看这一篇就够了。非常感谢原博主的翻译和分享。

对数据集进行分组并对各组应用一个函数(无论是聚合还是转换),通常是数据分析工作中的重要环节。在将数据集加载、融合、准备好之后,通常就是计算分组统计或生成透视表。pandas提供了一个灵活高效的gruopby功能,它使你能以一种自然的方式对数据集进行切片、切块、摘要等操作。

关系型数据库和SQL(Structured Query Language,结构化查询语言)能够如此流行的原因之一就是其能够方便地对数据进行连接、过滤、转换和聚合。但是,像SQL这样的查询语言所能执行的分组运算的种类很有限。在本章中你将会看到,由于Python和pandas强大的表达能力,我们可以执行复杂得多的分组运算(利用任何可以接受pandas对象或NumPy数组的函数)。在本章中,你将会学到:

  • 使用一个或多个键(形式可以是函数、数组或DataFrame列名)分割pandas对象。
  • 计算分组的概述统计,比如数量、平均值或标准差,或是用户定义的函数。
  • 应用组内转换或其他运算,如规格化、线性回归、排名或选取子集等。
  • 计算透视表或交叉表。
  • 执行分位数分析以及其它统计分组分析。

笔记:对时间序列数据的聚合(groupby的特殊用法之一)也称作重采样(resampling),本书将在第11章中单独对其进行讲解。

10.1 GroupBy机制

Hadley Wickham(许多热门R语言包的作者)创造了一个用于表示分组运算的术语"split-apply-combine"(拆分-应用-合并)。第一个阶段,pandas对象(无论是Series、DataFrame还是其他的)中的数据会根据你所提供的一个或多个键被拆分(split)为多组。拆分操作是在对象的特定轴上执行的。例如,DataFrame可以在其行(axis=0)或列(axis=1)上进行分组。然后,将一个函数应用(apply)到各个分组并产生一个新值。最后,所有这些函数的执行结果会被合并(combine)到最终的结果对象中。结果对象的形式一般取决于数据上所执行的操作。图10-1大致说明了一个简单的分组聚合过程。

图10-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)})
df
data1 data2 key1 key2
0 0.085071 -0.681321 a one
1 -0.320136 -1.545958 a two
2 0.692366 0.697484 b one
3 0.442214 1.022998 b two
4 -0.990618 0.514342 a one

假设你想要按key1进行分组,并计算data1列的平均值。实现该功能的方式有很多,而我们这里要用的是:访问data1,并根据key1调用groupby:

grouped=df['data1'].groupby(df['key1'])
grouped#SeriesGroupBy 对象
<pandas.core.groupby.groupby.SeriesGroupBy object at 0x000001104FECD0F0>

变量grouped是一个GroupBy对象。它实际上还没有进行任何计算,只是含有一些有关分组键df[‘key1’]的中间数据而已。换句话说,该对象已经有了接下来对各分组执行运算所需的一切信息。例如,我们可以调用GroupBy的mean方法来计算分组平均值:

grouped.mean()
key1
a   -0.408561
b    0.567290
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.452774
      two    -0.320136
b     one     0.692366
      two     0.442214
Name: data1, dtype: float64

这里,我通过两个键对数据进行了分组,得到的Series具有一个层次化索引(由唯一的键对组成):

means.unstack()
key2 one two
key1
a -0.452774 -0.320136
b 0.692366 0.442214

在这个例子中,分组键均为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.320136
            2006    0.692366
Ohio        2005    0.263643
            2006   -0.990618
Name: data1, dtype: float64

通常,分组信息就位于相同的要处理DataFrame中。这里,你还可以将列名(可以是字符串、数字或其他Python对象)用作分组键:这里可看作是特殊形式

df.groupby('key1').mean()
data1 data2
key1
a -0.408561 -0.570979
b 0.567290 0.860241
df.groupby(['key1','key2']).mean()
data1 data2
key1 key2
a one -0.452774 -0.083489
two -0.320136 -1.545958
b one 0.692366 0.697484
two 0.442214 1.022998

你可能已经注意到了,第一个例子在执行df.groupby(‘key1’).mean()时,结果中没有key2列。这是因为df[‘key2’]不是数值数据(俗称“麻烦列”),所以被从结果中排除了。默认情况下,所有数值列都会被聚合,虽然有时可能会被过滤为一个子集,稍后就会碰到。

无论你准备拿groupby做什么,都有可能会用到GroupBy的size方法(可以看作是apply后的特殊聚合函数),它可以返回一个含有分组大小的Series:

df.groupby(['key1','key2']).size()
key1  key2
a     one     2
      two     1
b     one     1
      two     1
dtype: int64

注意,任何分组关键词中的缺失值,都会被从结果中除去。

对分组进行迭代

GroupBy对象支持迭代,可以产生一组二元元组(由分组名和数据块组成)。看下面的例子:

for name,group in df.groupby('key1'):
    print(name)
    print(group)
a
      data1     data2 key1 key2
0  0.085071 -0.681321    a  one
1 -0.320136 -1.545958    a  two
4 -0.990618  0.514342    a  one
b
      data1     data2 key1 key2
2  0.692366  0.697484    b  one
3  0.442214  1.022998    b  two

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

for (k1,k2),group in df.groupby(['key1', 'key2']):
    print((k1,k2))
    print(group)
('a', 'one')
      data1     data2 key1 key2
0  0.085071 -0.681321    a  one
4 -0.990618  0.514342    a  one
('a', 'two')
      data1     data2 key1 key2
1 -0.320136 -1.545958    a  two
('b', 'one')
      data1     data2 key1 key2
2  0.692366  0.697484    b  one
('b', 'two')
      data1     data2 key1 key2
3  0.442214  1.022998    b  two

当然,你可以对这些数据片段做任何操作。有一个你可能会觉得有用的运算:将这些数据片段做成一个字典:

list(df.groupby('key1'))
[('a',       data1     data2 key1 key2
  0  0.085071 -0.681321    a  one
  1 -0.320136 -1.545958    a  two
  4 -0.990618  0.514342    a  one), ('b',       data1     data2 key1 key2
  2  0.692366  0.697484    b  one
  3  0.442214  1.022998    b  two)]
pieces = dict(list(df.groupby('key1')))
pieces
{'a':       data1     data2 key1 key2
 0  0.085071 -0.681321    a  one
 1 -0.320136 -1.545958    a  two
 4 -0.990618  0.514342    a  one, 'b':       data1     data2 key1 key2
 2  0.692366  0.697484    b  one
 3  0.442214  1.022998    b  two}

groupby默认是在axis=0上进行分组的,通过设置也可以在其他任何轴上进行分组。拿上面例子中的df来说,我们可以根据dtype对列进行分组:

df.dtypes
data1    float64
data2    float64
key1      object
key2      object
dtype: object
grouped=df.groupby(df.dtypes,axis=1)

可以如下打印分组:

for dtype, group in grouped:
    print(dtype)
    print(group)
float64
      data1     data2
0  0.085071 -0.681321
1 -0.320136 -1.545958
2  0.692366  0.697484
3  0.442214  1.022998
4 -0.990618  0.514342
object
  key1 key2
0    a  one
1    a  two
2    b  one
3    b  two
4    a  one

选取一列或列的子集

对于由DataFrame产生的GroupBy对象,如果用一个(单个字符串)或一组(字符串数组)列名对其进行索引,就能实现选取部分列进行聚合的目的。也就是说:
从下面也可以看到,df[‘data1’]会得到SeriesGroupBy 对象,而df[[‘data2’]]会得到DataFrameGroupBy 对象。

df.groupby('key1')['data1']
<pandas.core.groupby.groupby.SeriesGroupBy object at 0x000001105F09A198>
df.groupby('key1')[['data2']]
<pandas.core.groupby.groupby.DataFrameGroupBy object at 0x000001105F09AB38>

是以下代码的语法糖:

df['data1'].groupby(df['key1'])
df[['data2']].groupby(df['key1'])
<pandas.core.groupby.groupby.DataFrameGroupBy object at 0x000001105F09AC18>

尤其对于大数据集,很可能只需要对部分列进行聚合。例如,在前面那个数据集中,如果只需计算data2列的平均值并以DataFrame形式得到结果,可以这样写:

df.groupby(['key1', 'key2'])[['data2']].mean()
data2
key1 key2
a one -0.083489
two -1.545958
b one 0.697484
two 1.022998

这种索引操作所返回的对象是一个已分组的DataFrame(如果传入的是列表或数组)或已分组的Series(如果传入的是标量形式的单个列名):

s_grouped=df.groupby(['key1', 'key2'])['data2']
s_grouped
<pandas.core.groupby.groupby.SeriesGroupBy object at 0x000001105F09AD30>
s_grouped.mean()
key1  key2
a     one    -0.083489
      two    -1.545958
b     one     0.697484
      two     1.022998
Name: data2, dtype: float64

通过字典或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
people
a b c d e
Joe -1.816206 0.524415 1.445611 0.363604 -0.611044
Steve 0.096662 2.645132 0.657724 0.536647 -0.945192
Wes 0.075040 NaN NaN -2.047711 -0.903662
Jim -0.377932 -0.608861 -1.419598 -0.820359 -2.075691
Travis -0.290854 -0.252215 -0.823395 2.405941 -0.030716

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

mapping = {'a': 'red', 'b': 'red', 'c': 'blue', 'd': 'blue', 'e': 'red', 'f' : 'orange'}
by_column =people.groupby(mapping,axis=1)
for name,group in by_column:
    print(name)
    print(group)
blue
               c         d
Joe     1.445611  0.363604
Steve   0.657724  0.536647
Wes          NaN -2.047711
Jim    -1.419598 -0.820359
Travis -0.823395  2.405941
red
               a         b         e
Joe    -1.816206  0.524415 -0.611044
Steve   0.096662  2.645132 -0.945192
Wes     0.075040       NaN -0.903662
Jim    -0.377932 -0.608861 -2.075691
Travis -0.290854 -0.252215 -0.030716
by_column.sum()
blue red
Joe 1.809215 -1.902835
Steve 1.194371 1.796602
Wes -2.047711 -0.828622
Jim -2.239957 -3.062484
Travis 1.582547 -0.573784

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

通过函数进行分组

比起使用字典或Series,使用Python函数是一种更原生的方法定义分组映射。任何被当做分组键的函数都会在各个索引值上被调用一次,其返回值就会被用作分组名称。具体点说,以上一小节的示例DataFrame为例,其索引值为人的名字。你可以计算一个字符串长度的数组,更简单的方法是传入len函数:

people.groupby(len).sum()#len操作的是索引
a b c d e
3 -2.119099 -0.084446 0.026014 -2.504467 -3.590397
5 0.096662 2.645132 0.657724 0.536647 -0.945192
6 -0.290854 -0.252215 -0.823395 2.405941 -0.030716

将函数跟数组、列表、字典、Series混合使用也不是问题,因为任何东西在内部都会被转换为数组

key_list = ['one', 'one', 'one', 'two', 'two']
people.groupby([len,key_list]).min()
a b c d e
3 one -1.816206 0.524415 1.445611 -2.047711 -0.903662
two -0.377932 -0.608861 -1.419598 -0.820359 -2.075691
5 one 0.096662 2.645132 0.657724 0.536647 -0.945192
6 two -0.290854 -0.252215 -0.823395 2.405941 -0.030716

根据索引级别分组

层次化索引数据集最方便的地方就在于它能够根据轴索引的一个级别进行聚合:

columns=pd.MultiIndex.from_arrays([['US', 'US', 'US', 'JP', 'JP'],[1, 3, 5, 1, 3]],names=['cty','tenor'])
columns
MultiIndex(levels=[['JP', 'US'], [1, 3, 5]],
           labels=[[1, 1, 1, 0, 0], [0, 1, 2, 0, 1]],
           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 1.916257 1.689067 -0.518793 -0.390765 -0.863206
1 0.473669 0.544845 1.649351 0.841178 0.379657
2 0.957656 0.361352 -1.817090 -0.376896 -0.073625
3 0.619413 -0.051054 1.425500 -0.807275 1.486081

要根据级别分组,使用level关键字传递级别序号或名字:

hier_df.groupby(level='cty',axis=1).count()
cty JP US
0 2 3
1 2 3
2 2 3
3 2 3

10.2 数据聚合

聚合指的是任何能够从数组产生标量值的数据转换过程。之前的例子已经用过一些,比如mean、count、min以及sum等。你可能想知道在GroupBy对象上调用mean()时究竟发生了什么。许多常见的聚合运算(如表10-1所示)都有进行优化。然而,除了这些方法,你还可以使用其它的。

函数名 说明
count 分组中非NA值的数量
sum 非NA值的和
mean 非NA值的平均值
median 非NA值的算术中位数
std、var 无偏(分母为n-1)标准差和方差
min、max 非NA值的最小值和最大值
prod 非NA值的积
first、last 第一个和最后一个非NA值
表10-1 经过优化的groupby方法
你可以使用自己发明的聚合运算,还可以调用分组对象上已经定义好的任何方法。例如,quantile可以计算Series或DataFrame列的样本分位数。

虽然quantile并没有明确地实现于GroupBy,但它是一个Series方法,所以这里是能用的。实际上,GroupBy会高效地对Series进行切片,然后对各片调用piece.quantile(0.9),最后将这些结果组装成最终结果:

df
data1 data2 key1 key2
0 0.085071 -0.681321 a one
1 -0.320136 -1.545958 a two
2 0.692366 0.697484 b one
3 0.442214 1.022998 b two
4 -0.990618 0.514342 a one
grouped=df.groupby('key1')
grouped['data1'].quantile(0.9)
key1
a    0.004030
b    0.667351
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.075689 2.060300
b 0.250152 0.325513

你可能注意到注意,有些方法(如describe)也是可以用在这里的,即使严格来讲,它们并非聚合运算(其实也是聚合,只不过一下子完成了多种聚合):

grouped.describe()
data1 data2
count mean std min 25% 50% 75% max count mean std min 25% 50% 75% max
key1
a 3.0 -0.408561 0.543269 -0.990618 -0.655377 -0.320136 -0.117532 0.085071 3.0 -0.570979 1.034573 -1.545958 -1.113639 -0.681321 -0.083489 0.514342
b 2.0 0.567290 0.176884 0.442214 0.504752 0.567290 0.629828 0.692366 2.0 0.860241 0.230173 0.697484 0.778863 0.860241 0.941619 1.022998

在后面的10.3节,我将详细说明这到底是怎么回事。

笔记:自定义聚合函数要比表10-1中那些经过优化的函数慢得多。这是因为在构造中间分组数据块时存在非常大的开销(函数调用、数据重排等)。

面向列的多函数应用

回到前面小费的例子。使用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.head(5)
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

你已经看到,对Series或DataFrame列的聚合运算其实就是使用aggregate(使用自定义函数)或调用诸如mean、std之类的方法。然而,你可能希望对不同的列使用不同的聚合函数,或一次应用多个函数。其实这也好办,我将通过一些示例来进行讲解。首先,我根据天和smoker对tips进行分组:

grouped=tips.groupby(['smoker','day'])

注意,对于表10-1中的那些描述统计,可以将函数名以字符串的形式传入:

grouped_pct = grouped['tip_pct']
grouped_pct.agg('mean')
smoker  day 
No      Fri     0.151650
        Sat     0.158048
        Sun     0.160113
        Thur    0.160298
Yes     Fri     0.174783
        Sat     0.147906
        Sun     0.187250
        Thur    0.163863
Name: tip_pct, dtype: float64

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

grouped_pct.agg(['mean','std',peak_to_peak])
mean std peak_to_peak
smoker day
No Fri 0.151650 0.028123 0.067349
Sat 0.158048 0.039767 0.235193
Sun 0.160113 0.042347 0.193226
Thur 0.160298 0.038774 0.193350
Yes Fri 0.174783 0.051293 0.159925
Sat 0.147906 0.061375 0.290095
Sun 0.187250 0.154134 0.644685
Thur 0.163863 0.039389 0.151240

这里,我们传递了一组聚合函数进行聚合,独立对数据分组进行评估。

你并非一定要接受GroupBy自动给出的那些列名,特别是lambda函数,它们的名称是’’,这样的辨识度就很低了(通过函数的name属性看看就知道了)。因此,如果传入的是一个由(name,function)元组组成的列表,则各元组的第一个元素就会被用作DataFrame的列名(可以将这种二元元组列表看做一个有序映射):

grouped_pct.agg([('foo','mean'),('bar','std')])
foo bar
smoker day
No Fri 0.151650 0.028123
Sat 0.158048 0.039767
Sun 0.160113 0.042347
Thur 0.160298 0.038774
Yes Fri 0.174783 0.051293
Sat 0.147906 0.061375
Sun 0.187250 0.154134
Thur 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
smoker day
No Fri 4 0.151650 0.187735 4 18.420000 22.75
Sat 45 0.158048 0.291990 45 19.661778 48.33
Sun 57 0.160113 0.252672 57 20.506667 48.17
Thur 45 0.160298 0.266312 45 17.113111 41.19
Yes Fri 15 0.174783 0.263480 15 16.813333 40.17
Sat 42 0.147906 0.325733 42 21.276667 50.81
Sun 19 0.187250 0.710345 19 24.120000 45.35
Thur 17 0.163863 0.241255 17 19.190588 43.11

如你所见,结果DataFrame拥有层次化的列,这相当于分别对各列进行聚合,然后用concat将结果组装到一起,使用列名用作keys参数:

result['tip_pct']
count mean max
smoker day
No Fri 4 0.151650 0.187735
Sat 45 0.158048 0.291990
Sun 57 0.160113 0.252672
Thur 45 0.160298 0.266312
Yes Fri 15 0.174783 0.263480
Sat 42 0.147906 0.325733
Sun 19 0.187250 0.710345
Thur 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
smoker day
No Fri 0.151650 0.000791 18.420000 25.596333
Sat 0.158048 0.001581 19.661778 79.908965
Sun 0.160113 0.001793 20.506667 66.099980
Thur 0.160298 0.001503 17.113111 59.625081
Yes Fri 0.174783 0.002631 16.813333 82.562438
Sat 0.147906 0.003767 21.276667 101.387535
Sun 0.187250 0.023757 24.120000 109.046044
Thur 0.163863 0.001551 19.190588 69.808518

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

grouped.agg({'tip':np.max,'size':'sum'})
size tip
smoker day
No Fri 9 3.50
Sat 115 9.00
Sun 167 6.00
Thur 112 6.70
Yes Fri 31 4.73
Sat 104 10.00
Sun 49 6.50
Thur 40 5.00
grouped.agg({'tip':['min','max','mean','std'],'size':'sum'})
size tip
sum min max mean std
smoker day
No Fri 9 1.50 3.50 2.812500 0.898494
Sat 115 1.00 9.00 3.102889 1.642088
Sun 167 1.01 6.00 3.167895 1.224785
Thur 112 1.25 6.70 2.673778 1.282964
Yes Fri 31 1.00 4.73 2.714000 1.077668
Sat 104 1.00 10.00 2.875476 1.630580
Sun 49 1.50 6.50 3.516842 1.261151
Thur 40 2.00 5.00 3.030000 1.113491

只有将多个函数应用到至少一列时,DataFrame才会拥有层次化的列。

以“没有行索引”的形式返回聚合数据

到目前为止,所有示例中的聚合数据都有由唯一的分组键组成的索引(可能还是层次化的)。由于并不总是需要如此,所以你可以向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方法可以避免一些不必要的计算。

10.3 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:]
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)#top处理的是各个group
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% max
smoker
No 151.0 0.159328 0.039910 0.056797 0.136906 0.155625 0.185014 0.291990
Yes 93.0 0.163196 0.085119 0.035638 0.106771 0.153846 0.195059 0.710345
result.unstack()
       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)
total_bill tip size tip_pct
smoker day
No Fri count 4.000000 4.000000 4.000000 4.000000
mean 18.420000 2.812500 2.250000 0.151650
std 5.059282 0.898494 0.500000 0.028123
min 12.460000 1.500000 2.000000 0.120385
25% 15.100000 2.625000 2.000000 0.137239
50% 19.235000 3.125000 2.000000 0.149241
75% 22.555000 3.312500 2.250000 0.163652
max 22.750000 3.500000 3.000000 0.187735
Sat count 45.000000 45.000000 45.000000 45.000000
mean 19.661778 3.102889 2.555556 0.158048
std 8.939181 1.642088 0.784960 0.039767
min 7.250000 1.000000 1.000000 0.056797
25% 14.730000 2.010000 2.000000 0.136240
50% 17.820000 2.750000 2.000000 0.150152
75% 20.650000 3.390000 3.000000 0.183915
max 48.330000 9.000000 4.000000 0.291990
Sun count 57.000000 57.000000 57.000000 57.000000
mean 20.506667 3.167895 2.929825 0.160113
std 8.130189 1.224785 1.032674 0.042347
min 8.770000 1.010000 2.000000 0.059447
25% 14.780000 2.000000 2.000000 0.139780
50% 18.430000 3.020000 3.000000 0.161665
75% 25.000000 3.920000 4.000000 0.185185
max 48.170000 6.000000 6.000000 0.252672
Thur count 45.000000 45.000000 45.000000 45.000000
mean 17.113111 2.673778 2.488889 0.160298
std 7.721728 1.282964 1.179796 0.038774
min 7.510000 1.250000 1.000000 0.072961
25% 11.690000 1.800000 2.000000 0.137741
50% 15.950000 2.180000 2.000000 0.153492
... ... ... ... ... ... ...
Yes Fri std 9.086388 1.077668 0.593617 0.051293
min 5.750000 1.000000 1.000000 0.103555
25% 11.690000 1.960000 2.000000 0.133739
50% 13.420000 2.500000 2.000000 0.173913
75% 18.665000 3.240000 2.000000 0.209240
max 40.170000 4.730000 4.000000 0.263480
Sat count 42.000000 42.000000 42.000000 42.000000
mean 21.276667 2.875476 2.476190 0.147906
std 10.069138 1.630580 0.862161 0.061375
min 3.070000 1.000000 1.000000 0.035638
25% 13.405000 2.000000 2.000000 0.091797
50% 20.390000 2.690000 2.000000 0.153624
75% 26.792500 3.197500 3.000000 0.190502
max 50.810000 10.000000 5.000000 0.325733
Sun count 19.000000 19.000000 19.000000 19.000000
mean 24.120000 3.516842 2.578947 0.187250
std 10.442511 1.261151 0.901591 0.154134
min 7.250000 1.500000 2.000000 0.065660
25% 17.165000 3.000000 2.000000 0.097723
50% 23.100000 3.500000 2.000000 0.138122
75% 32.375000 4.000000 3.000000 0.215325
max 45.350000 6.500000 5.000000 0.710345
Thur count 17.000000 17.000000 17.000000 17.000000
mean 19.190588 3.030000 2.352941 0.163863
std 8.355149 1.113491 0.701888 0.039389
min 10.340000 2.000000 2.000000 0.090014
25% 13.510000 2.000000 2.000000 0.148038
50% 16.470000 2.560000 2.000000 0.153846
75% 19.810000 4.000000 2.000000 0.194837
max 43.110000 5.000000 4.000000 0.241255

64 rows × 4 columns

禁止分组键

从上面的例子中可以看出,分组键会跟原始对象的索引共同构成结果对象中的层次化索引。将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

分位数和桶分析

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

frame = pd.DataFrame({'data1': np.random.randn(1000),'data2': np.random.randn(1000)})
frame.head(5)
data1 data2
0 -0.750938 -0.004619
1 -0.516941 -0.045986
2 -0.454464 1.016261
3 0.571966 -0.025783
4 -0.638771 0.310319
quartiles=pd.cut(frame.data1,4)
quartiles
0      (-1.514, -0.0748]
1      (-1.514, -0.0748]
2      (-1.514, -0.0748]
3       (-0.0748, 1.365]
4      (-1.514, -0.0748]
5       (-0.0748, 1.365]
6      (-1.514, -0.0748]
7       (-0.0748, 1.365]
8      (-1.514, -0.0748]
9      (-1.514, -0.0748]
10     (-1.514, -0.0748]
11       (-2.96, -1.514]
12      (-0.0748, 1.365]
13      (-0.0748, 1.365]
14     (-1.514, -0.0748]
15      (-0.0748, 1.365]
16     (-1.514, -0.0748]
17      (-0.0748, 1.365]
18     (-1.514, -0.0748]
19     (-1.514, -0.0748]
20      (-0.0748, 1.365]
21      (-0.0748, 1.365]
22     (-1.514, -0.0748]
23      (-0.0748, 1.365]
24     (-1.514, -0.0748]
25     (-1.514, -0.0748]
26      (-0.0748, 1.365]
27     (-1.514, -0.0748]
28     (-1.514, -0.0748]
29      (-0.0748, 1.365]
             ...        
970    (-1.514, -0.0748]
971    (-1.514, -0.0748]
972     (-0.0748, 1.365]
973    (-1.514, -0.0748]
974      (-2.96, -1.514]
975     (-0.0748, 1.365]
976       (1.365, 2.804]
977     (-0.0748, 1.365]
978     (-0.0748, 1.365]
979     (-0.0748, 1.365]
980    (-1.514, -0.0748]
981    (-1.514, -0.0748]
982      (-2.96, -1.514]
983       (1.365, 2.804]
984    (-1.514, -0.0748]
985     (-0.0748, 1.365]
986     (-0.0748, 1.365]
987      (-2.96, -1.514]
988    (-1.514, -0.0748]
989    (-1.514, -0.0748]
990    (-1.514, -0.0748]
991    (-1.514, -0.0748]
992    (-1.514, -0.0748]
993     (-0.0748, 1.365]
994       (1.365, 2.804]
995    (-1.514, -0.0748]
996     (-0.0748, 1.365]
997    (-1.514, -0.0748]
998     (-0.0748, 1.365]
999      (-2.96, -1.514]
Name: data1, Length: 1000, dtype: category
Categories (4, interval[float64]): [(-2.96, -1.514] < (-1.514, -0.0748] < (-0.0748, 1.365] < (1.365, 2.804]]

由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)
grouped.apply(get_stats)
data1                   
(-2.96, -1.514]    count     61.000000
                   max        2.171292
                   mean      -0.213579
                   min       -2.263387
(-1.514, -0.0748]  count    429.000000
                   max        2.939454
                   mean       0.010478
                   min       -2.832253
(-0.0748, 1.365]   count    428.000000
                   max        3.053561
                   mean       0.008130
                   min       -2.734429
(1.365, 2.804]     count     82.000000
                   max        2.790070
                   mean      -0.047075
                   min       -2.233048
Name: data2, dtype: float64
grouped.apply(get_stats).unstack()
count max mean min
data1
(-2.96, -1.514] 61.0 2.171292 -0.213579 -2.263387
(-1.514, -0.0748] 429.0 2.939454 0.010478 -2.832253
(-0.0748, 1.365] 428.0 3.053561 0.008130 -2.734429
(1.365, 2.804] 82.0 2.790070 -0.047075 -2.233048

这些都是长度相等的桶。要根据样本分位数得到大小相等的桶,使用qcut即可。传入labels=False即可只获取分位数的编号:

grouping =pd.qcut(frame.data1,10,labels=False)
grouped = frame.data2.groupby(grouping)
grouped.apply(get_stats).unstack()
count max mean min
data1
0 100.0 2.292859 -0.060392 -2.263387
1 100.0 2.939454 -0.037872 -2.832253
2 100.0 2.655457 0.218327 -2.276501
3 100.0 2.183310 -0.078582 -2.349690
4 100.0 2.238415 -0.099075 -2.654970
5 100.0 2.088735 0.022876 -2.489442
6 100.0 2.596358 0.065066 -2.131919
7 100.0 2.300451 -0.067696 -2.697164
8 100.0 3.053561 -0.026999 -2.734429
9 100.0 2.790070 -0.024792 -2.233048

我们会在第12章详细讲解pandas的Categorical类型。

示例:用特定于分组的值填充缺失值

对于缺失数据的清理工作,有时你会用dropna将其替换掉,而有时则可能会希望用一个固定值或由数据集本身所衍生出来的值去填充NA值。这时就得使用fillna这个工具了。在下面这个例子中,我用平均值去填充NA值:

s = pd.Series(np.random.randn(6))
s[::2]=np.nan
s
0         NaN
1    0.290080
2         NaN
3    0.285803
4         NaN
5    1.184106
dtype: float64
s.fillna(s.mean())
0    0.586663
1    0.290080
2    0.586663
3    0.285803
4    0.586663
5    1.184106
dtype: float64

假设你需要对不同的分组填充不同的值。一种方法是将数据分组,并使用apply和一个能够对各数据块调用fillna的函数即可。下面是一些有关美国几个州的示例数据,这些州又被分为东部和西部:

states = ['Ohio', 'New York', 'Vermont', 'Florida','Oregon', 'Nevada', 'California', 'Idaho']
group_key =['East']*4+['West']*4
group_key
['East', 'East', 'East', 'East', 'West', 'West', 'West', 'West']
data = pd.Series(np.random.randn(8),index=states)
data
Ohio          0.884782
New York      0.477073
Vermont      -0.505671
Florida       1.379208
Oregon        1.458109
Nevada       -0.701916
California    0.027076
Idaho        -0.561943
dtype: float64

[‘East’] * 4产生了一个列表,包括了[‘East’]中元素的四个拷贝。将这些列表串联起来。

将一些值设为缺失:

data[['Vermont', 'Nevada', 'Idaho']] = np.nan
data
Ohio          0.884782
New York      0.477073
Vermont            NaN
Florida       1.379208
Oregon        1.458109
Nevada             NaN
California    0.027076
Idaho              NaN
dtype: float64
data.groupby(group_key).mean()
East    0.913688
West    0.742592
dtype: float64

我们可以用分组平均值去填充NA值:

fill_mean = lambda g:g.fillna(g.mean())#这里返回的不是一个标题,仍然是一个group,所以后面可以还原成原series等长的series
data.groupby(group_key).apply(fill_mean)
Ohio          0.884782
New York      0.477073
Vermont       0.913688
Florida       1.379208
Oregon        1.458109
Nevada        0.742592
California    0.027076
Idaho         0.742592
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          0.884782
New York      0.477073
Vermont       0.500000
Florida       1.379208
Oregon        1.458109
Nevada       -1.000000
California    0.027076
Idaho        -1.000000
dtype: float64

示例:随机采样和排列

假设你想要从一个大数据集中随机抽取(进行替换或不替换)样本以进行蒙特卡罗模拟(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']
base_names
['A', 2, 3, 4, 5, 6, 7, 8, 9, 10, 'J', 'K', 'Q']
cards = []
for suit in ['H', 'S', 'C', 'D']:
    cards.extend(str(num) + suit for num in base_names)
cards
['AH',
 '2H',
 '3H',
 '4H',
 '5H',
 '6H',
 '7H',
 '8H',
 '9H',
 '10H',
 'JH',
 'KH',
 'QH',
 'AS',
 '2S',
 '3S',
 '4S',
 '5S',
 '6S',
 '7S',
 '8S',
 '9S',
 '10S',
 'JS',
 'KS',
 'QS',
 'AC',
 '2C',
 '3C',
 '4C',
 '5C',
 '6C',
 '7C',
 '8C',
 '9C',
 '10C',
 'JC',
 'KC',
 'QC',
 'AD',
 '2D',
 '3D',
 '4D',
 '5D',
 '6D',
 '7D',
 '8D',
 '9D',
 '10D',
 'JD',
 'KD',
 'QD']
deck = pd.Series(card_val, index=cards)
deck
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
AS      1
2S      2
3S      3
4S      4
5S      5
6S      6
7S      7
8S      8
9S      9
10S    10
JS     10
KS     10
QS     10
AC      1
2C      2
3C      3
4C      4
5C      5
6C      6
7C      7
8C      8
9C      9
10C    10
JC     10
KC     10
QC     10
AD      1
2D      2
3D      3
4D      4
5D      5
6D      6
7D      7
8D      8
9D      9
10D    10
JD     10
KD     10
QD     10
dtype: int64

现在我有了一个长度为52的Series,其索引包括牌名,值则是21点或其他游戏中用于计分的点数(为了简单起见,我当A的点数为1):

现在,根据我上面所讲的,从整副牌中抽出5张,代码如下:

def draw(deck,n=5):
    return deck.sample(n)
draw(deck)
6D     6
2S     2
4D     4
KH    10
6H     6
dtype: int64

假设你想要从每种花色中随机抽取两张牌。由于花色是牌名的最后一个字符,所以我们可以据此进行分组,并使用apply:

get_suit = lambda card:card[-1]# last letter is suit
deck.groupby(get_suit).apply(draw,n=2)
C  9C      9
   4C      4
D  10D    10
   9D      9
H  7H      7
   4H      4
S  QS     10
   9S      9
dtype: int64

或者,也可以这样写:

deck.groupby(get_suit,group_keys=False).apply(draw,n=2)
AC      1
QC     10
10D    10
9D      9
QH     10
4H      4
9S      9
8S      8
dtype: int64

示例:分组加权平均数和相关系数

根据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.258813 0.570950
1 a 0.014016 0.537235
2 a 0.692250 0.693037
3 a -0.050724 0.351212
4 b -0.027253 0.876652
5 b -1.488066 0.252720
6 b 1.582432 0.709194
7 b 1.708540 0.985476

然后可以利用category计算分组加权平均数:

grouped=df.groupby('category')
get_wavg=lambda g:np.average(g['data'],weights=g['weights'])
grouped.apply(get_wavg)
category
a    0.149459
b    0.851977
dtype: float64

另一个例子,考虑一个来自Yahoo!Finance的数据集,其中含有几只股票和标准普尔500指数(符号SPX)的收盘价:

close_px=pd.read_csv('examples/stock_px_2.csv',parse_dates=True,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'])

接下来,我们使用pct_change计算close_px的百分比变化:

rets = close_px.pct_change().dropna()
rets
AAPL MSFT XOM SPX
2003-01-03 0.006757 0.001421 0.000684 -0.000484
2003-01-06 0.000000 0.017975 0.024624 0.022474
2003-01-07 -0.002685 0.019052 -0.033712 -0.006545
2003-01-08 -0.020188 -0.028272 -0.004145 -0.014086
2003-01-09 0.008242 0.029094 0.021159 0.019386
2003-01-10 0.002725 0.001824 -0.013927 0.000000
2003-01-13 -0.005435 0.008648 -0.004134 -0.001412
2003-01-14 -0.002732 0.010379 0.008993 0.005830
2003-01-15 -0.010959 -0.012506 -0.013713 -0.014426
2003-01-16 0.012465 -0.016282 0.004519 -0.003942
2003-01-17 -0.035568 -0.070345 -0.010381 -0.014017
2003-01-21 -0.005674 -0.002473 -0.023077 -0.015702
2003-01-22 -0.009986 -0.006445 -0.012885 -0.010432
2003-01-23 0.021614 0.024950 -0.002175 0.010224
2003-01-24 -0.026798 -0.046251 -0.021439 -0.029233
2003-01-27 0.024638 -0.013783 -0.026736 -0.016160
2003-01-28 0.031117 -0.007246 0.026326 0.013050
2003-01-29 0.024691 0.022419 0.036431 0.006779
2003-01-30 -0.041499 -0.033656 -0.018293 -0.022849
2003-01-31 0.002793 -0.015831 0.027768 0.013130
2003-02-03 0.020891 0.023056 0.013864 0.005399
2003-02-04 -0.004093 -0.025681 0.000000 -0.014088
2003-02-05 -0.010959 -0.007531 -0.014376 -0.005435
2003-02-06 0.000000 0.009756 -0.008538 -0.006449
2003-02-07 -0.020776 -0.017713 -0.007535 -0.010094
2003-02-10 0.015559 0.017486 0.007592 0.007569
2003-02-11 0.000000 -0.019871 -0.007176 -0.008098
2003-02-12 0.002786 0.000000 -0.019877 -0.012687
2003-02-13 0.009722 0.011507 0.012906 -0.001600
2003-02-14 0.009629 0.028169 0.009465 0.021435
... ... ... ... ...
2011-09-02 -0.018319 -0.015643 -0.018370 -0.025282
2011-09-06 0.015212 -0.011240 -0.013723 -0.007436
2011-09-07 0.011034 0.019208 0.035137 0.028646
2011-09-08 0.000547 0.008462 -0.011270 -0.010612
2011-09-09 -0.017337 -0.018307 -0.024856 -0.026705
2011-09-12 0.006517 0.005828 0.011688 0.006966
2011-09-13 0.012318 0.005794 -0.002645 0.009120
2011-09-14 0.012168 0.017665 0.013817 0.013480
2011-09-15 0.009401 0.018491 0.018860 0.017187
2011-09-16 0.019188 0.004817 0.007296 0.005707
2011-09-19 0.027790 0.003319 -0.011402 -0.009803
2011-09-20 0.004421 -0.008453 0.004206 -0.001661
2011-09-21 -0.003168 -0.036694 -0.027564 -0.029390
2011-09-22 -0.025040 -0.035783 -0.037932 -0.031883
2011-09-23 0.006172 0.000000 0.001011 0.006082
2011-09-26 -0.002795 0.015164 0.034771 0.023336
2011-09-27 -0.009698 0.009041 0.016592 0.010688
2011-09-28 -0.005635 -0.003506 -0.011521 -0.020691
2011-09-29 -0.016221 -0.005082 0.025114 0.008114
2011-09-30 -0.023683 -0.022004 -0.016919 -0.024974
2011-10-03 -0.017623 -0.014464 -0.020377 -0.028451
2011-10-04 -0.005606 0.033021 0.023612 0.022488
2011-10-05 0.015436 0.021705 0.015378 0.017866
2011-10-06 -0.002327 0.017381 -0.000811 0.018304
2011-10-07 -0.020060 -0.003417 -0.004466 -0.008163
2011-10-10 0.051406 0.026286 0.036977 0.034125
2011-10-11 0.029526 0.002227 -0.000131 0.000544
2011-10-12 0.004747 -0.001481 0.011669 0.009795
2011-10-13 0.015515 0.008160 -0.010238 -0.002974
2011-10-14 0.033225 0.003311 0.022784 0.017380

2213 rows × 4 columns

最后,我们用年对百分比变化进行分组,可以用一个一行的函数,从每行的标签返回每个datetime标签的year属性:

get_year = lambda x:x.year
by_year = rets.groupby(get_year)
by_year.apply(spx_corr)
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

当然,你还可以计算列与列之间的相关系数。这里,我们计算Apple和Microsoft的年相关系数:

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

示例:组级别的线性回归

顺着上一个例子继续,你可以用groupby执行更为复杂的分组统计分析,只要函数返回的是pandas对象或标量值即可。例如,我可以定义下面这个regress函数(利用statsmodels计量经济学库)对各数据块执行普通最小二乘法(Ordinary Least Squares,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, yvar='AAPL', xvars=['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

10.4 透视表和交叉表

透视表(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',margins=True,fill_value=0)
day Fri Sat Sun Thur All
time size smoker
Dinner 1 No 0.000000 0.137931 0.000000 0.000000 0.137931
Yes 0.000000 0.325733 0.000000 0.000000 0.325733
2 No 0.139622 0.162705 0.168859 0.159744 0.164383
Yes 0.171297 0.148668 0.207893 0.000000 0.167246
3 No 0.000000 0.154661 0.152663 0.000000 0.153705
Yes 0.000000 0.144995 0.152660 0.000000 0.148061
4 No 0.000000 0.150096 0.148143 0.000000 0.148737
Yes 0.117750 0.124515 0.193370 0.000000 0.139064
5 No 0.000000 0.000000 0.206928 0.000000 0.206928
Yes 0.000000 0.106572 0.065660 0.000000 0.086116
6 No 0.000000 0.000000 0.103799 0.000000 0.103799
Lunch 1 No 0.000000 0.000000 0.000000 0.181728 0.181728
Yes 0.223776 0.000000 0.000000 0.000000 0.223776
2 No 0.000000 0.000000 0.000000 0.166005 0.166005
Yes 0.181969 0.000000 0.000000 0.158843 0.165266
3 No 0.187735 0.000000 0.000000 0.084246 0.118742
Yes 0.000000 0.000000 0.000000 0.204952 0.204952
4 No 0.000000 0.000000 0.000000 0.138919 0.138919
Yes 0.000000 0.000000 0.000000 0.155410 0.155410
5 No 0.000000 0.000000 0.000000 0.121389 0.121389
6 No 0.000000 0.000000 0.000000 0.173706 0.173706
All 0.169913 0.153152 0.166897 0.161276 0.160803

pivot_table的参数说明请参见表10-2。

函数名 说明
values 待聚合的列的名称。默认聚合所有数值列
index 用于分组的列名或其他分组键,出现在结果透视表的行
columns 用于分组的列名或其他分组键,出现在结果透视表的列
aggfunc 聚合函数或函数列表,默认为mean。可以是任何对 groupby有效的函数
fill_value 用于替换结果表中的缺失值
drona 如果为TrUe,不添加条目都为NA的列
margins 添加行列小计和总计,默认为Fase
表10-2 pivot_table的选项

交叉表:crosstab

交叉表(cross-tabulation,简称crosstab)是一种用于计算分组频率的特殊透视表。看下面的例子:

data=pd.DataFrame({'Sample':np.arange(1,11),'Nationality':['USA','Japan','USA','Japan','Japan','Japan','USA','USA','Japan','USA'],'Handedness':['Right-handed','Left-handed','Right-handed','Right-handed','Left-handed','Right-handed','Right-handed','Left-handed','Right-handed','Right-handed']})
data
Handedness Nationality Sample
0 Right-handed USA 1
1 Left-handed Japan 2
2 Right-handed USA 3
3 Right-handed Japan 4
4 Left-handed Japan 5
5 Right-handed Japan 6
6 Right-handed USA 7
7 Left-handed USA 8
8 Right-handed Japan 9
9 Right-handed USA 10

作为调查分析的一部分,我们可能想要根据国籍和用手习惯对这段数据进行统计汇总。虽然可以用pivot_table实现该功能,但是pandas.crosstab函数会更方便:

pd.crosstab(data.Nationality,data.Handedness,margins=True)
Handedness Left-handed Right-handed All
Nationality
Japan 2 3 5
USA 1 4 5
All 3 7 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

10.5 总结

掌握pandas数据分组工具既有助于数据清理,也有助于建模或统计分析工作。在第14章,我们会看几个例子,对真实数据使用groupby。

在下一章,我们将关注时间序列数据。

猜你喜欢

转载自blog.csdn.net/qingqing7/article/details/83109969