第十篇 数据聚合与分组运算

对数据集进⾏分组并对各组应⽤⼀个函数(⽆论是聚合还是转换),通常是数据分析⼯作中的重要环节。在将数据集加载、融合、准备好之后,通常就是计算分组统计或⽣成透视表。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⼤致说明了⼀个简单的分组聚合过程。

image
                                      图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

扫描二维码关注公众号,回复: 4655169 查看本文章

当然,你可以对这些数据⽚段做任何操作。有⼀个你可能会觉得有⽤的运算:将这些数据⽚段做成⼀个字典:
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所示)都有进⾏优化。然⽽,除了这些⽅法,你还可以使⽤其它的。

表10-1 经过优化的groupby⽅法
image

你可以使⽤⾃⼰发明的聚合运算,还可以调⽤分组对象上已经定义好的任何⽅法。例如,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会将待处理的对象拆分成多个⽚段,然后对各⽚段调⽤传⼊的函数,最后尝试将各⽚段组合到⼀起
image
                                   图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数据分组⼯具既有助于数据清理,也有助于建模或统计分析⼯作。

猜你喜欢

转载自www.cnblogs.com/Micro0623/p/10178114.html
今日推荐