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


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

    当然,你可以对这些数据⽚段做任何操作。有⼀个你可能会觉得有⽤的运算:将这些数据⽚段做成⼀个字典:
    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数据分组⼯具既有助于数据清理,也有助于建模或统计分析⼯作。

  • 相关阅读:
    .NET Framework类库大概
    CTS、CLS、CIL与CLR
    .NETFramework、CLR、.NET Framework class library、托管代码是什么
    万维网三大核心技术
    Clang、GCC和LLVM是什么
    常见开源许可证、开源协议GPL、BSD、MIT、Mozilla、Apache和LGPL之间区别
    编译原理之变量、名字与标识符
    编译原理之静态策略与动态策略
    Jupyter Notebook打开任意文件
    修改VsCodes的语言为中文
  • 原文地址:https://www.cnblogs.com/Micro0623/p/10178114.html
Copyright © 2020-2023  润新知