数据基础---《利用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
data1data2key1key2
00.085071-0.681321aone
1-0.320136-1.545958atwo
20.6923660.697484bone
30.4422141.022998btwo
4-0.9906180.514342aone

假设你想要按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()
key2onetwo
key1
a-0.452774-0.320136
b0.6923660.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()
data1data2
key1
a-0.408561-0.570979
b0.5672900.860241
df.groupby(['key1','key2']).mean()
data1data2
key1key2
aone-0.452774-0.083489
two-0.320136-1.545958
bone0.6923660.697484
two0.4422141.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
key1key2
aone-0.083489
two-1.545958
bone0.697484
two1.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
abcde
Joe-1.8162060.5244151.4456110.363604-0.611044
Steve0.0966622.6451320.6577240.536647-0.945192
Wes0.075040NaNNaN-2.047711-0.903662
Jim-0.377932-0.608861-1.419598-0.820359-2.075691
Travis-0.290854-0.252215-0.8233952.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()
bluered
Joe1.809215-1.902835
Steve1.1943711.796602
Wes-2.047711-0.828622
Jim-2.239957-3.062484
Travis1.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()
bluered
Joe23
Steve23
Wes12
Jim23
Travis23

通过函数进行分组

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

people.groupby(len).sum()#len操作的是索引
abcde
3-2.119099-0.0844460.026014-2.504467-3.590397
50.0966622.6451320.6577240.536647-0.945192
6-0.290854-0.252215-0.8233952.405941-0.030716

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

key_list = ['one', 'one', 'one', 'two', 'two']
people.groupby([len,key_list]).min()
abcde
3one-1.8162060.5244151.445611-2.047711-0.903662
two-0.377932-0.608861-1.419598-0.820359-2.075691
5one0.0966622.6451320.6577240.536647-0.945192
6two-0.290854-0.252215-0.8233952.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
ctyUSJP
tenor13513
01.9162571.689067-0.518793-0.390765-0.863206
10.4736690.5448451.6493510.8411780.379657
20.9576560.361352-1.817090-0.376896-0.073625
30.619413-0.0510541.425500-0.8072751.486081

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

hier_df.groupby(level='cty',axis=1).count()
ctyJPUS
023
123
223
323

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
data1data2key1key2
00.085071-0.681321aone
1-0.320136-1.545958atwo
20.6923660.697484bone
30.4422141.022998btwo
4-0.9906180.514342aone
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)
data1data2
key1
a1.0756892.060300
b0.2501520.325513

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

grouped.describe()
data1data2
countmeanstdmin25%50%75%maxcountmeanstdmin25%50%75%max
key1
a3.0-0.4085610.543269-0.990618-0.655377-0.320136-0.1175320.0850713.0-0.5709791.034573-1.545958-1.113639-0.681321-0.0834890.514342
b2.00.5672900.1768840.4422140.5047520.5672900.6298280.6923662.00.8602410.2301730.6974840.7788630.8602410.9416191.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_billtipsmokerdaytimesizetip_pct
016.991.01NoSunDinner20.059447
110.341.66NoSunDinner30.160542
221.013.50NoSunDinner30.166587
323.683.31NoSunDinner20.139780
424.593.61NoSunDinner40.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])
meanstdpeak_to_peak
smokerday
NoFri0.1516500.0281230.067349
Sat0.1580480.0397670.235193
Sun0.1601130.0423470.193226
Thur0.1602980.0387740.193350
YesFri0.1747830.0512930.159925
Sat0.1479060.0613750.290095
Sun0.1872500.1541340.644685
Thur0.1638630.0393890.151240

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

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

grouped_pct.agg([('foo','mean'),('bar','std')])
foobar
smokerday
NoFri0.1516500.028123
Sat0.1580480.039767
Sun0.1601130.042347
Thur0.1602980.038774
YesFri0.1747830.051293
Sat0.1479060.061375
Sun0.1872500.154134
Thur0.1638630.039389

对于DataFrame,你还有更多选择,你可以定义一组应用于全部列的一组函数,或不同的列应用不同的函数。假设我们想要对tip_pct和total_bill列计算三个统计信息:

functions = ['count', 'mean', 'max']
result = grouped['tip_pct','total_bill'].agg(functions)
result
tip_pcttotal_bill
countmeanmaxcountmeanmax
smokerday
NoFri40.1516500.187735418.42000022.75
Sat450.1580480.2919904519.66177848.33
Sun570.1601130.2526725720.50666748.17
Thur450.1602980.2663124517.11311141.19
YesFri150.1747830.2634801516.81333340.17
Sat420.1479060.3257334221.27666750.81
Sun190.1872500.7103451924.12000045.35
Thur170.1638630.2412551719.19058843.11

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

result['tip_pct']
countmeanmax
smokerday
NoFri40.1516500.187735
Sat450.1580480.291990
Sun570.1601130.252672
Thur450.1602980.266312
YesFri150.1747830.263480
Sat420.1479060.325733
Sun190.1872500.710345
Thur170.1638630.241255

跟前面一样,这里也可以传入带有自定义名称的一组元组:

ftuples = [('Durchschnitt', 'mean'),('Abweichung', np.var)]
grouped['tip_pct', 'total_bill'].agg(ftuples)
tip_pcttotal_bill
DurchschnittAbweichungDurchschnittAbweichung
smokerday
NoFri0.1516500.00079118.42000025.596333
Sat0.1580480.00158119.66177879.908965
Sun0.1601130.00179320.50666766.099980
Thur0.1602980.00150317.11311159.625081
YesFri0.1747830.00263116.81333382.562438
Sat0.1479060.00376721.276667101.387535
Sun0.1872500.02375724.120000109.046044
Thur0.1638630.00155119.19058869.808518

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

grouped.agg({'tip':np.max,'size':'sum'})
sizetip
smokerday
NoFri93.50
Sat1159.00
Sun1676.00
Thur1126.70
YesFri314.73
Sat10410.00
Sun496.50
Thur405.00
grouped.agg({'tip':['min','max','mean','std'],'size':'sum'})
sizetip
summinmaxmeanstd
smokerday
NoFri91.503.502.8125000.898494
Sat1151.009.003.1028891.642088
Sun1671.016.003.1678951.224785
Thur1121.256.702.6737781.282964
YesFri311.004.732.7140001.077668
Sat1041.0010.002.8754761.630580
Sun491.506.503.5168421.261151
Thur402.005.003.0300001.113491

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

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

到目前为止,所有示例中的聚合数据都有由唯一的分组键组成的索引(可能还是层次化的)。由于并不总是需要如此,所以你可以向groupby传入as_index=False以禁用该功能:

tips.groupby(['day', 'smoker'],as_index=False).mean()
daysmokertotal_billtipsizetip_pct
0FriNo18.4200002.8125002.2500000.151650
1FriYes16.8133332.7140002.0666670.174783
2SatNo19.6617783.1028892.5555560.158048
3SatYes21.2766672.8754762.4761900.147906
4SunNo20.5066673.1678952.9298250.160113
5SunYes24.1200003.5168422.5789470.187250
6ThurNo17.1131112.6737782.4888890.160298
7ThurYes19.1905883.0300002.3529410.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_billtipsmokerdaytimesizetip_pct
10914.314.00YesSatDinner20.279525
18323.176.50YesSunDinner40.280535
23211.613.39NoSatDinner20.291990
673.071.00YesSatDinner10.325733
1789.604.00YesSunDinner20.416667
1727.255.15YesSunDinner20.710345

现在,如果对smoker分组并用该函数调用apply,就会得到:

tips.groupby('smoker').apply(top)#top处理的是各个group
total_billtipsmokerdaytimesizetip_pct
smoker
No8824.715.85NoThurLunch20.236746
18520.695.00NoSunDinner50.241663
5110.292.60NoSunDinner20.252672
1497.512.00NoThurLunch20.266312
23211.613.39NoSatDinner20.291990
Yes10914.314.00YesSatDinner20.279525
18323.176.50YesSunDinner40.280535
673.071.00YesSatDinner10.325733
1789.604.00YesSunDinner20.416667
1727.255.15YesSunDinner20.710345

这里发生了什么?top函数在DataFrame的各个片段上调用,然后结果由pandas.concat组装到一起,并以分组名称进行了标记。于是,最终结果就有了一个层次化索引,其内层索引值来自原DataFrame。
如果传给apply的函数能够接受其他参数或关键字,则可以将这些内容放在函数名后面一并传入:

tips.groupby(['smoker','day']).apply(top,n=1,column='total_bill')
total_billtipsmokerdaytimesizetip_pct
smokerday
NoFri9422.753.25NoFriDinner20.142857
Sat21248.339.00NoSatDinner40.186220
Sun15648.175.00NoSunDinner60.103799
Thur14241.195.00NoThurLunch50.121389
YesFri9540.174.73YesFriDinner40.117750
Sat17050.8110.00YesSatDinner30.196812
Sun18245.353.50YesSunDinner30.077178
Thur19743.115.00YesThurLunch40.115982

笔记:除这些基本用法之外,能否充分发挥apply的威力很大程度上取决于你的创造力。传入的那个函数能做什么全由你说了算,它只需返回一个pandas对象或标量值即可。本章后续部分的示例主要用于讲解如何利用groupby解决各种各样的问题。
可能你已经想起来了,之前我在GroupBy对象上调用过describe:

result=tips.groupby('smoker')['tip_pct'].describe()
result
countmeanstdmin25%50%75%max
smoker
No151.00.1593280.0399100.0567970.1369060.1556250.1850140.291990
Yes93.00.1631960.0851190.0356380.1067710.1538460.1950590.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_billtipsizetip_pct
smokerday
NoFricount4.0000004.0000004.0000004.000000
mean18.4200002.8125002.2500000.151650
std5.0592820.8984940.5000000.028123
min12.4600001.5000002.0000000.120385
25%15.1000002.6250002.0000000.137239
50%19.2350003.1250002.0000000.149241
75%22.5550003.3125002.2500000.163652
max22.7500003.5000003.0000000.187735
Satcount45.00000045.00000045.00000045.000000
mean19.6617783.1028892.5555560.158048
std8.9391811.6420880.7849600.039767
min7.2500001.0000001.0000000.056797
25%14.7300002.0100002.0000000.136240
50%17.8200002.7500002.0000000.150152
75%20.6500003.3900003.0000000.183915
max48.3300009.0000004.0000000.291990
Suncount57.00000057.00000057.00000057.000000
mean20.5066673.1678952.9298250.160113
std8.1301891.2247851.0326740.042347
min8.7700001.0100002.0000000.059447
25%14.7800002.0000002.0000000.139780
50%18.4300003.0200003.0000000.161665
75%25.0000003.9200004.0000000.185185
max48.1700006.0000006.0000000.252672
Thurcount45.00000045.00000045.00000045.000000
mean17.1131112.6737782.4888890.160298
std7.7217281.2829641.1797960.038774
min7.5100001.2500001.0000000.072961
25%11.6900001.8000002.0000000.137741
50%15.9500002.1800002.0000000.153492
.....................
YesFristd9.0863881.0776680.5936170.051293
min5.7500001.0000001.0000000.103555
25%11.6900001.9600002.0000000.133739
50%13.4200002.5000002.0000000.173913
75%18.6650003.2400002.0000000.209240
max40.1700004.7300004.0000000.263480
Satcount42.00000042.00000042.00000042.000000
mean21.2766672.8754762.4761900.147906
std10.0691381.6305800.8621610.061375
min3.0700001.0000001.0000000.035638
25%13.4050002.0000002.0000000.091797
50%20.3900002.6900002.0000000.153624
75%26.7925003.1975003.0000000.190502
max50.81000010.0000005.0000000.325733
Suncount19.00000019.00000019.00000019.000000
mean24.1200003.5168422.5789470.187250
std10.4425111.2611510.9015910.154134
min7.2500001.5000002.0000000.065660
25%17.1650003.0000002.0000000.097723
50%23.1000003.5000002.0000000.138122
75%32.3750004.0000003.0000000.215325
max45.3500006.5000005.0000000.710345
Thurcount17.00000017.00000017.00000017.000000
mean19.1905883.0300002.3529410.163863
std8.3551491.1134910.7018880.039389
min10.3400002.0000002.0000000.090014
25%13.5100002.0000002.0000000.148038
50%16.4700002.5600002.0000000.153846
75%19.8100004.0000002.0000000.194837
max43.1100005.0000004.0000000.241255

64 rows × 4 columns

禁止分组键

从上面的例子中可以看出,分组键会跟原始对象的索引共同构成结果对象中的层次化索引。将group_keys=False传入groupby即可禁止该效果:

tips.groupby('smoker',group_keys=False).apply(top)
total_billtipsmokerdaytimesizetip_pct
8824.715.85NoThurLunch20.236746
18520.695.00NoSunDinner50.241663
5110.292.60NoSunDinner20.252672
1497.512.00NoThurLunch20.266312
23211.613.39NoSatDinner20.291990
10914.314.00YesSatDinner20.279525
18323.176.50YesSunDinner40.280535
673.071.00YesSatDinner10.325733
1789.604.00YesSunDinner20.416667
1727.255.15YesSunDinner20.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)
data1data2
0-0.750938-0.004619
1-0.516941-0.045986
2-0.4544641.016261
30.571966-0.025783
4-0.6387710.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()
countmaxmeanmin
data1
(-2.96, -1.514]61.02.171292-0.213579-2.263387
(-1.514, -0.0748]429.02.9394540.010478-2.832253
(-0.0748, 1.365]428.03.0535610.008130-2.734429
(1.365, 2.804]82.02.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()
countmaxmeanmin
data1
0100.02.292859-0.060392-2.263387
1100.02.939454-0.037872-2.832253
2100.02.6554570.218327-2.276501
3100.02.183310-0.078582-2.349690
4100.02.238415-0.099075-2.654970
5100.02.0887350.022876-2.489442
6100.02.5963580.065066-2.131919
7100.02.300451-0.067696-2.697164
8100.03.053561-0.026999-2.734429
9100.02.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
categorydataweights
0a-0.2588130.570950
1a0.0140160.537235
2a0.6922500.693037
3a-0.0507240.351212
4b-0.0272530.876652
5b-1.4880660.252720
6b1.5824320.709194
7b1.7085400.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:]
AAPLMSFTXOMSPX
2011-10-11400.2927.0076.271195.54
2011-10-12402.1926.9677.161207.25
2011-10-13408.4327.1876.371203.66
2011-10-14422.0027.2778.111224.58

来做一个比较有趣的任务:计算一个由日收益率(通过百分数变化计算)与SPX之间的年度相关系数组成的DataFrame。下面是一个实现办法,我们先创建一个函数,用它计算每列和SPX列的成对相关系数:

spx_corr = lambda x:x.corrwith(x['SPX'])

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

rets = close_px.pct_change().dropna()
rets
AAPLMSFTXOMSPX
2003-01-030.0067570.0014210.000684-0.000484
2003-01-060.0000000.0179750.0246240.022474
2003-01-07-0.0026850.019052-0.033712-0.006545
2003-01-08-0.020188-0.028272-0.004145-0.014086
2003-01-090.0082420.0290940.0211590.019386
2003-01-100.0027250.001824-0.0139270.000000
2003-01-13-0.0054350.008648-0.004134-0.001412
2003-01-14-0.0027320.0103790.0089930.005830
2003-01-15-0.010959-0.012506-0.013713-0.014426
2003-01-160.012465-0.0162820.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-230.0216140.024950-0.0021750.010224
2003-01-24-0.026798-0.046251-0.021439-0.029233
2003-01-270.024638-0.013783-0.026736-0.016160
2003-01-280.031117-0.0072460.0263260.013050
2003-01-290.0246910.0224190.0364310.006779
2003-01-30-0.041499-0.033656-0.018293-0.022849
2003-01-310.002793-0.0158310.0277680.013130
2003-02-030.0208910.0230560.0138640.005399
2003-02-04-0.004093-0.0256810.000000-0.014088
2003-02-05-0.010959-0.007531-0.014376-0.005435
2003-02-060.0000000.009756-0.008538-0.006449
2003-02-07-0.020776-0.017713-0.007535-0.010094
2003-02-100.0155590.0174860.0075920.007569
2003-02-110.000000-0.019871-0.007176-0.008098
2003-02-120.0027860.000000-0.019877-0.012687
2003-02-130.0097220.0115070.012906-0.001600
2003-02-140.0096290.0281690.0094650.021435
...............
2011-09-02-0.018319-0.015643-0.018370-0.025282
2011-09-060.015212-0.011240-0.013723-0.007436
2011-09-070.0110340.0192080.0351370.028646
2011-09-080.0005470.008462-0.011270-0.010612
2011-09-09-0.017337-0.018307-0.024856-0.026705
2011-09-120.0065170.0058280.0116880.006966
2011-09-130.0123180.005794-0.0026450.009120
2011-09-140.0121680.0176650.0138170.013480
2011-09-150.0094010.0184910.0188600.017187
2011-09-160.0191880.0048170.0072960.005707
2011-09-190.0277900.003319-0.011402-0.009803
2011-09-200.004421-0.0084530.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-230.0061720.0000000.0010110.006082
2011-09-26-0.0027950.0151640.0347710.023336
2011-09-27-0.0096980.0090410.0165920.010688
2011-09-28-0.005635-0.003506-0.011521-0.020691
2011-09-29-0.016221-0.0050820.0251140.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.0056060.0330210.0236120.022488
2011-10-050.0154360.0217050.0153780.017866
2011-10-06-0.0023270.017381-0.0008110.018304
2011-10-07-0.020060-0.003417-0.004466-0.008163
2011-10-100.0514060.0262860.0369770.034125
2011-10-110.0295260.002227-0.0001310.000544
2011-10-120.004747-0.0014810.0116690.009795
2011-10-130.0155150.008160-0.010238-0.002974
2011-10-140.0332250.0033110.0227840.017380

2213 rows × 4 columns

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

get_year = lambda x:x.year
by_year = rets.groupby(get_year)
by_year.apply(spx_corr)
AAPLMSFTXOMSPX
20030.5411240.7451740.6612651.0
20040.3742830.5885310.5577421.0
20050.4675400.5623740.6310101.0
20060.4282670.4061260.5185141.0
20070.5081180.6587700.7862641.0
20080.6814340.8046260.8283031.0
20090.7071030.6549020.7979211.0
20100.7101050.7301180.8390571.0
20110.6919310.8009960.8599751.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'])
SPXintercept
20031.1954060.000710
20041.3634630.004201
20051.7664150.003246
20061.6454960.000080
20071.1987610.003438
20080.968016-0.001110
20090.8791030.002954
20101.0526080.001261
20110.8066050.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'])
sizetiptip_pcttotal_bill
daysmoker
FriNo2.2500002.8125000.15165018.420000
Yes2.0666672.7140000.17478316.813333
SatNo2.5555563.1028890.15804819.661778
Yes2.4761902.8754760.14790621.276667
SunNo2.9298253.1678950.16011320.506667
Yes2.5789473.5168420.18725024.120000
ThurNo2.4888892.6737780.16029817.113111
Yes2.3529413.0300000.16386319.190588

可以用groupby直接来做。现在,假设我们只想聚合tip_pct和size,而且想根据time进行分组。我将smoker放到列上,把day放到行上:#在行列上都进行了分组

tips.pivot_table(['tip_pct','size'],index=['time','day'],columns='smoker')
sizetip_pct
smokerNoYesNoYes
timeday
DinnerFri2.0000002.2222220.1396220.165347
Sat2.5555562.4761900.1580480.147906
Sun2.9298252.5789470.1601130.187250
Thur2.000000NaN0.159744NaN
LunchFri3.0000001.8333330.1877350.188937
Thur2.5000002.3529410.1603110.163863

还可以对这个表作进一步的处理,传入margins=True添加分项小计。这将会添加标签为All的行和列,其值对应于单个等级中所有数据的分组统计:

tips.pivot_table(['tip_pct','size'],index=['time','day'],columns='smoker',margins=True)
sizetip_pct
smokerNoYesAllNoYesAll
timeday
DinnerFri2.0000002.2222222.1666670.1396220.1653470.158916
Sat2.5555562.4761902.5172410.1580480.1479060.153152
Sun2.9298252.5789472.8421050.1601130.1872500.166897
Thur2.000000NaN2.0000000.159744NaN0.159744
LunchFri3.0000001.8333332.0000000.1877350.1889370.188765
Thur2.5000002.3529412.4590160.1603110.1638630.161301
All2.6688742.4086022.5696720.1593280.1631960.160803

这里,All值为平均数:不单独考虑烟民与非烟民(All列),不单独考虑行分组两个级别中的任何单项(All行)。

要使用其他的聚合函数,将其传给aggfunc即可。例如,使用count或len可以得到有关分组大小的交叉表(计数或频率):

tips.pivot_table('tip_pct',index=['time','smoker'],columns='day',aggfunc=len,margins=True)
dayFriSatSunThurAll
timesmoker
DinnerNo3.045.057.01.0106.0
Yes9.042.019.0NaN70.0
LunchNo1.0NaNNaN44.045.0
Yes6.0NaNNaN17.023.0
All19.087.076.062.0244.0

如果存在空的组合(也就是NA),你可能会希望设置一个fill_value:

tips.pivot_table('tip_pct',index=['time','size','smoker'],columns='day',aggfunc='mean',margins=True,fill_value=0)
dayFriSatSunThurAll
timesizesmoker
Dinner1No0.0000000.1379310.0000000.0000000.137931
Yes0.0000000.3257330.0000000.0000000.325733
2No0.1396220.1627050.1688590.1597440.164383
Yes0.1712970.1486680.2078930.0000000.167246
3No0.0000000.1546610.1526630.0000000.153705
Yes0.0000000.1449950.1526600.0000000.148061
4No0.0000000.1500960.1481430.0000000.148737
Yes0.1177500.1245150.1933700.0000000.139064
5No0.0000000.0000000.2069280.0000000.206928
Yes0.0000000.1065720.0656600.0000000.086116
6No0.0000000.0000000.1037990.0000000.103799
Lunch1No0.0000000.0000000.0000000.1817280.181728
Yes0.2237760.0000000.0000000.0000000.223776
2No0.0000000.0000000.0000000.1660050.166005
Yes0.1819690.0000000.0000000.1588430.165266
3No0.1877350.0000000.0000000.0842460.118742
Yes0.0000000.0000000.0000000.2049520.204952
4No0.0000000.0000000.0000000.1389190.138919
Yes0.0000000.0000000.0000000.1554100.155410
5No0.0000000.0000000.0000000.1213890.121389
6No0.0000000.0000000.0000000.1737060.173706
All0.1699130.1531520.1668970.1612760.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
HandednessNationalitySample
0Right-handedUSA1
1Left-handedJapan2
2Right-handedUSA3
3Right-handedJapan4
4Left-handedJapan5
5Right-handedJapan6
6Right-handedUSA7
7Left-handedUSA8
8Right-handedJapan9
9Right-handedUSA10

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

pd.crosstab(data.Nationality,data.Handedness,margins=True)
HandednessLeft-handedRight-handedAll
Nationality
Japan235
USA145
All3710

crosstab的前两个参数可以是数组或Series,或是数组列表。就像小费数据:

pd.crosstab([tips.time,tips.day],tips.smoker,margins=True)
smokerNoYesAll
timeday
DinnerFri3912
Sat454287
Sun571976
Thur101
LunchFri167
Thur441761
All15193244

10.5 总结

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

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

Python常用的几种去重方式

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值