Pandas入门(中)

Pandas入门(中)

Martin

GroupBy技术

“split-apply-combine”(拆分-应用-合并),这个几个词很好的描述了分组运算的整个过程。分组运算的第一个阶段,pandas对象(无论是Series是还DataFrame抑或其他)中的数据会根据你所提供的一个或者多个键被拆分(split)为多组。拆分操作是在对象的特定轴上执行的。例如,DataFrame可以在其行(axis=0)或列(axis=1)上进行分组。然后,将一个函数应用apply到各个分组并产生一个新值。最后所有这些函数的执行结果会被合并(combine)到最终的结果对象中。结果对象的形式一般取决于数据上所执行的操作。下图可以大致说明一个简单的分组聚合过程:

1.png-20.7kB
分组可以有多种形式,且类型不必相同:

  • 列表或数组,其长度与待分组的轴一样。
  • 表示DataFrame某个列名的值。
  • 字典或Series,给出待分组轴上的值与分组名之间的对应关系。
  • 函数,用于处理轴索引或索引中的各标签。

后三种都是快捷方式而已,其最终目的仍是产生一组用于拆分对象的值。
来个例子:

>>> df = DataFrame({'key1':['a','a','b','b','a'],'key2':['one','two','one','two','one'],'data1':np.random.randn(5),'data2':np.random.randn(5)})

>>> df
      data1     data2 key1 key2
0 -0.237560 -0.070793    a  one
1  0.285330 -1.242159    a  two
2 -0.447567 -0.850921    b  one
3  2.644768 -1.463350    b  two
4 -1.062439  0.454708    a  one

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

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

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

>>> grouped.mean()
key1
a   -0.338223
b    1.098601
Name: data1, dtype: float64

一会将详细讲解mean方法,这里最重要的是,数据(Series)根据分组键进行了聚合,产生了一个新的Series,其索引为key1列总的唯一值。之所以结果中索引的名称为key1,是因为原始的DataFramedf['key1']就叫这个名字。如果我们一次传入多个数组,就会得到不同的结果:

>>> means = df['data1'].groupby([df['key1'],df['key2']]).mean()
>>> means
key1  key2
a     one    -0.650000
      two     0.285330
b     one    -0.447567
      two     2.644768
Name: data1, dtype: float64

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

>>> means.unstack()                 # 之前讲的unstack方法
key2       one       two
key1                    
a    -0.650000  0.285330
b    -0.447567  2.644768

在上面的例子中,分组键均为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.285330
            2006   -0.447567
Ohio        2005    1.203604
            2006   -1.062439
Name: data1, dtype: float64

此外,还可以将列名当成分组键:

>>> df.groupby('key1').mean()   # 未指明哪个数据列就是默认全部(data1和data2)
         data1     data2
key1                    
a    -0.338223 -0.286081
b     1.098601 -1.157135

可能已经注意到了,在执行df.groupby('key1').mean()时,结果中没有key2列。这是因为df['key2']不是数值数据(俗称麻烦列),所以被从结果中排除了。默认情况下,所有数值列都会被聚合

无论准备拿groupby做什么,都有可能用到GroupBysize方法,它可以返回一个含有分组大小的Series

>>> df
      data1     data2 key1 key2
0  0.676933 -0.320961    a  one
1 -0.695488  0.151366    a  two
2 -1.268873 -1.337475    b  one
3 -0.369639  1.246996    b  two
4  1.193304 -0.607347    a  one

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

对分组进行迭代

GroupBy对象虽然保存了一些中间数据,但是不可见,不过它却支持迭代,可以产生一组二元元组(由分组名和数据块组成),还是上例子说话再:)

>>> df.groupby('key1')
<pandas.core.groupby.DataFrameGroupBy object at 0x7f3cc35f5410>

>>> for name,group in df.groupby('key1'):       # GroupBy对象迭代
...     print name
...     print group
... 
a                                           # 分组名1
      data1     data2 key1 key2             # 数据块1
0  0.676933 -0.320961    a  one
1 -0.695488  0.151366    a  two
4  1.193304 -0.607347    a  one
b                                           # 分组名2
      data1     data2 key1 key2             # 数据块2
2 -1.268873 -1.337475    b  one
3 -0.369639  1.246996    b  two

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

>>> for (k1,k2),group in df.groupby(['key1','key2']):
...     print k1,k2
...     print group
... 
a one
      data1     data2 key1 key2
0  0.676933 -0.320961    a  one
4  1.193304 -0.607347    a  one
a two
      data1     data2 key1 key2
1 -0.695488  0.151366    a  two
b one
      data1     data2 key1 key2
2 -1.268873 -1.337475    b  one
b two
      data1     data2 key1 key2
3 -0.369639  1.246996    b  two

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

>>> pieces = dict(list(df.groupby('key1')))
>>> pieces['a']
      data1     data2 key1 key2
0  0.676933 -0.320961    a  one
1 -0.695488  0.151366    a  two
4  1.193304 -0.607347    a  one

>>> pieces['b']
      data1     data2 key1 key2
2 -1.268873 -1.337475    b  one
3 -0.369639  1.246996    b  two

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

>>> df.dtypes
data1    float64
data2    float64
key1      object
key2      object
dtype: object

>>> dict(list(df.groupby(df.dtypes,axis=1)))
{dtype('O'):   key1 key2
0    a  one
1    a  two
2    b  one
3    b  two
4    a  one, dtype('float64'):       data1     data2
0  0.676933 -0.320961
1 -0.695488  0.151366
2 -1.268873 -1.337475
3 -0.369639  1.246996
4  1.193304 -0.607347}

选取一个或一组列

对于由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.464154
     two   0.151366
b    one  -1.337475
     two   1.246996

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

>>> type(df.groupby(['key1','key2'])['data2'])   # 标量,返回Series
<class 'pandas.core.groupby.SeriesGroupBy'>

>>> type(df.groupby(['key1','key2'])[['data2']]) # 列表,返回DataFrame
<class 'pandas.core.groupby.DataFrameGroupBy'>

通过字典或Series进行分组

除数组以外,分组信息还可以其他形式存在。来看另一个例子:

>>> people = DataFrame(np.random.randn(5,5),columns=['a','b','c','d','e'],index=['Joe','Steve','Wes','Jim','Travis'])
>>> people
               a         b         c         d         e
Joe     1.194226  1.847886  2.063846  1.227742  1.087048
Steve  -0.950449 -0.176224 -0.646874  0.381825 -1.584446
Wes     1.195563 -3.315661 -0.393060  0.590954 -0.428106
Jim     1.344869  1.364735  0.227360  0.195137  0.325883
Travis -0.232074  1.602136 -0.018983 -0.887190  0.554802

>>> people.ix[2:3,['b','c']] = np.nan
>>> people
               a         b         c         d         e
Joe     1.194226  1.847886  2.063846  1.227742  1.087048
Steve  -0.950449 -0.176224 -0.646874  0.381825 -1.584446
Wes     1.195563       NaN       NaN  0.590954 -0.428106
Jim     1.344869  1.364735  0.227360  0.195137  0.325883
Travis -0.232074  1.602136 -0.018983 -0.887190  0.554802

>>> mapping = {'a':'red','b':'red','c':'blue','d':'blue','e':'red','f':'orange'}

现在只需将这个字典传给groupby即可:

>>> by_column = people.groupby(mapping,axis=1)
>>> by_column.sum()
            blue       red
Joe     3.291588  4.129159
Steve  -0.265049 -2.711119
Wes     0.590954  0.767457
Jim     0.422497  3.035487
Travis -0.906173  1.924865

还可以这样

>>> mapping2 = {'Joe':'one','Steve':'two','Wes':'one','Jim':'three','Travis':'two'}
>>> by_row = people.groupby(mapping2,axis=0)
>>> by_row.sum()
              a         b         c         d         e
one    2.389788  1.847886  2.063846  1.818696  0.658942
three  1.344869  1.364735  0.227360  0.195137  0.325883
two   -1.182522  1.425912 -0.665857 -0.505364 -1.029644

可能你也注意到了,我之前添加了几个NA值,进行sum后都自动过滤了。

Series也有同样地功能,它可以被看做是一个固定大小的映射。对于上面的例子,如果用Series作为分组键,则pandas会检查Series以确保其索引跟分组轴是对齐的:

>>> map_series = Series(mapping)    # 先将dict转换成Series
>>> map_series
a       red
b       red
c      blue
d      blue
e       red
f    orange
dtype: object
>>> people.groupby(map_series,axis=1).count()
        blue  red
Joe        2    3
Steve      2    3
Wes        1    2
Jim        2    3
Travis     2    3

通过函数进行分组

相较于字典或者Series,python函数在定义分组映射关系时可以更有创意且更为抽象。任何被当做分组键的函数都会在各个索引值上被调用一次,其返回至就会被用作分组名称。来个例子:

>>> people.groupby(len).sum()
          a         b         c         d         e
3  3.734657  3.212620  2.291206  2.013833  0.984826
5 -0.950449 -0.176224 -0.646874  0.381825 -1.584446
6 -0.232074  1.602136 -0.018983 -0.887190  0.554802

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

>>> key_list = ['one','one','one','two','two']
>>> people.groupby([len,key_list]).min()
              a         b         c         d         e
3 one  1.194226  1.847886  2.063846  0.590954 -0.428106
  two  1.344869  1.364735  0.227360  0.195137  0.325883
5 one -0.950449 -0.176224 -0.646874  0.381825 -1.584446
6 two -0.232074  1.602136 -0.018983 -0.887190  0.554802

根据索引级别分组

层次化索引数据集最方便的地方就在于它能够根据索引级别进行聚合。要实现该目的通过关键字key传入级别编号或名称即可:

>>> columns = pd.MultiIndex.from_arrays([['US','US','US','JP','JP'],[1,3,5,1,3]],names=['city','tenor'])
>>> hier_df = DataFrame(np.random.randn(4,5),columns=columns)
>>> hier_df
city         US                            JP          
tenor         1         3         5         1         3
0      0.844416 -0.746479  0.260580 -0.529815  1.127544
1     -0.286859 -0.305911 -0.488367 -1.634609  0.145259
2      0.857976 -1.911634  0.732285  1.290351  0.824145
3      0.274936 -0.527338 -0.586308  0.996981  0.387922

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

数据聚合

如果你要使用自己的聚合函数,只需要将其传入aggregateagg方法即可:

>>> def peak_to_peak(arr):
...     return arr.max() -  arr.min()
... 
>>> grouped.agg(peak_to_peak)
key1
a    2.621552
b    1.243450
Name: data1, dtype: float64

面向列的多函数应用

已经看到了,对SeriesDataFrame列的聚合运算其实就是使用aggregateagg或者调用诸如meanstd之类的方法。然而,我们更希望可以对不同的列使用不同的聚合函数,或一次应用多个函数。

在接下来,将使用一个有关餐馆小费的数据集:

>>> tips = pd.read_csv('下载/book_data/ch08/tips.csv')  # 文件存放的路径

添加消费占总额百分比tips_pct列

>>> tips['tip_pct'] = tips['tip'] / tips['total_bill']

>>> tips[:6]
   total_bill   tip     sex smoker  day    time  size   tip_pct
0       16.99  1.01  Female     No  Sun  Dinner     2  0.059447
1       10.34  1.66    Male     No  Sun  Dinner     3  0.160542
2       21.01  3.50    Male     No  Sun  Dinner     3  0.166587
3       23.68  3.31    Male     No  Sun  Dinner     2  0.139780
4       24.59  3.61  Female     No  Sun  Dinner     4  0.146808
5       25.29  4.71    Male     No  Sun  Dinner     4  0.186240

现在根据sexsmoker进行分组:

>>> grouped = tips.groupby(['sex','smoker'])
>>> grouped_pct = grouped['tip_pct']
>>> grouped_pct.agg('mean')
sex     smoker
Female  No        0.156921
        Yes       0.182150
Male    No        0.160669
        Yes       0.152771
Name: tip_pct, dtype: float64

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

>>> grouped_pct.agg(['mean','std'])
                   mean       std
sex    smoker                    
Female No      0.156921  0.036421
       Yes     0.182150  0.071595
Male   No      0.160669  0.041849
       Yes     0.152771  0.090588

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

>>> grouped_pct.agg([('foo','mean'),('bar',np.std)])
                    foo       bar
sex    smoker                    
Female No      0.156921  0.036421
       Yes     0.182150  0.071595
Male   No      0.160669  0.041849
       Yes     0.152771  0.090588

对于DataFrame,你还可以定义一组应用于全部列的函数,或不同的列应用不同的函数,假设我们想要对tip_pcttotal-bill列计算三个统计信息:

>>> functions = ['count','mean','max']
>>> result = grouped['tip_pct','total_bill'].agg(functions)
>>> result
              tip_pct                     total_bill                  
                count      mean       max      count       mean    max
sex    smoker                                                         
Female No          54  0.156921  0.252672         54  18.105185  35.83
       Yes         33  0.182150  0.416667         33  17.977879  44.30
Male   No          97  0.160669  0.291990         97  19.791237  48.33
       Yes         60  0.152771  0.710345         60  22.284500  50.81

如结果所示,DataFrame拥有层次化的列,这相当于分别对各列进行整合,然后用concat(连接函数)将结果组装到一起(列名用作keys参数)。

>>> result['tip_pct']
               count      mean       max
sex    smoker                           
Female No         54  0.156921  0.252672
       Yes        33  0.182150  0.416667
Male   No         97  0.160669  0.291990
       Yes        60  0.152771  0.710345

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

>>> ftuples = [('Durchschnitt','mean'),('Abweichung',np.var)]
>>> grouped['tip_pct','total_bill'].agg(ftuples)
                   tip_pct              total_bill           
              Durchschnitt Abweichung Durchschnitt Abweichung
sex    smoker                                                
Female No         0.156921   0.001327    18.105185  53.092422
       Yes        0.182150   0.005126    17.977879  84.451517
Male   No         0.160669   0.001751    19.791237  76.152961
       Yes        0.152771   0.008206    22.284500  98.244673

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

>>> grouped.agg({'tip':np.max,'size':'sum'})
                tip  size
sex    smoker            
Female No       5.2   140
       Yes      6.5    74
Male   No       9.0   263
       Yes     10.0   150

以“无索引”形式返回聚合数据

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

>>> tips.groupby(['sex','smoker'],as_index=False).mean()    # 禁用
      sex smoker  total_bill       tip      size   tip_pct
0  Female     No   18.105185  2.773519  2.592593  0.156921
1  Female    Yes   17.977879  2.931515  2.242424  0.182150
2    Male     No   19.791237  3.113402  2.711340  0.160669
3    Male    Yes   22.284500  3.051167  2.500000  0.152771

>>> tips.groupby(['sex','smoker']).mean()                   # 未禁用
               total_bill       tip      size   tip_pct
sex    smoker                                          
Female No       18.105185  2.773519  2.592593  0.156921
       Yes      17.977879  2.931515  2.242424  0.182150
Male   No       19.791237  3.113402  2.711340  0.160669
       Yes      22.284500  3.051167  2.500000  0.152771

分组级运算和转换

聚合只不过是分组运算的一种而已。它是数据转换的一个特例。也就是说它接受能够将一维数组简化为标量值的函数。在本节中,将介绍transformapply,它们能够执行更多其他分组运算。

假设我们想要为一个DataFrame添加一个用于存放各索引分组平均值的列。一个办法是先聚合再合并:

>>> df
      data1     data2 key1 key2
0  1.289781  0.439950    a  one
1 -0.337467 -0.560815    a  two
2 -0.222298 -1.229548    b  one
3  0.136132  0.293326    b  two
4 -1.433943 -2.416655    a  one

>>> k1_means = df.groupby('key1').mean().add_prefix('mean')
>>> k1_means
      meandata1  meandata2          # 列名加了前缀
key1                      
a     -0.160543  -0.845840
b     -0.043083  -0.468111

>>> pd.merge(df,k1_means,left_on='key1',right_index=True)
      data1     data2 key1 key2  meandata1  meandata2
0  1.289781  0.439950    a  one  -0.160543  -0.845840
1 -0.337467 -0.560815    a  two  -0.160543  -0.845840
4 -1.433943 -2.416655    a  one  -0.160543  -0.845840
2 -0.222298 -1.229548    b  one  -0.043083  -0.468111
3  0.136132  0.293326    b  two  -0.043083  -0.468111

merge函数是进行表的连接操作,如果不明白其中参数的意思可以使用help(pd.merge)命令进行查询。

虽然这样也行,但是不太灵活。你可以将该过程看做利用np.mean函数对两个数据列进行行方向广播转换。再以之前用过的那个peopleDataFrame为例,这次在GroupBy上使用transform方法:

>>> key = ['one','two','one','two','one']
>>> people.groupby(key).mean()
           a         b         c         d         e
one -0.16407 -0.006354 -0.404462  0.611442 -0.206593
two -0.06096  0.516483 -0.036140  0.349761 -0.649544

>>> people.groupby(key).transform(np.mean)
              a         b         c         d         e
Joe    -0.16407 -0.006354 -0.404462  0.611442 -0.206593
Steve  -0.06096  0.516483 -0.036140  0.349761 -0.649544
Wes    -0.16407 -0.006354 -0.404462  0.611442 -0.206593
Jim    -0.06096  0.516483 -0.036140  0.349761 -0.649544
Travis -0.16407 -0.006354 -0.404462  0.611442 -0.206593

不难看出,transform会将一个函数应用到各个分组,然后将结果放置到适当的位置上。如果各分组产生的是一个标量值,则该值就会被广播出去。

apply:一般性的“拆分-应用-合并”

回到之前的那个小费的数据集:

>>> tips[:7]
   total_bill   tip     sex smoker  day    time  size   tip_pct
0       16.99  1.01  Female     No  Sun  Dinner     2  0.059447
1       10.34  1.66    Male     No  Sun  Dinner     3  0.160542
2       21.01  3.50    Male     No  Sun  Dinner     3  0.166587
3       23.68  3.31    Male     No  Sun  Dinner     2  0.139780
4       24.59  3.61  Female     No  Sun  Dinner     4  0.146808
5       25.29  4.71    Male     No  Sun  Dinner     4  0.186240
6        8.77  2.00    Male     No  Sun  Dinner     2  0.228050

假设你想要根据分组选出最高的5个tip_pct值,首先,编写一个选取指定列具有最大值的行的函数:

>>> def top(df,n=5,column='tip_pct'):
...     return df.sort_values(by=column)[-n:]
... 
>>> top(tips,n=6)
     total_bill   tip     sex smoker  day    time  size   tip_pct
109       14.31  4.00  Female    Yes  Sat  Dinner     2  0.279525
183       23.17  6.50    Male    Yes  Sun  Dinner     4  0.280535
232       11.61  3.39    Male     No  Sat  Dinner     2  0.291990
67         3.07  1.00  Female    Yes  Sat  Dinner     1  0.325733
178        9.60  4.00  Female    Yes  Sun  Dinner     2  0.416667
172        7.25  5.15    Male    Yes  Sun  Dinner     2  0.710345

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

>>> tips.groupby('smoker').apply(top)
            total_bill   tip     sex smoker   day    time  size   tip_pct
smoker                                                                   
No     88        24.71  5.85    Male     No  Thur   Lunch     2  0.236746
       185       20.69  5.00    Male     No   Sun  Dinner     5  0.241663
       51        10.29  2.60  Female     No   Sun  Dinner     2  0.252672
       149        7.51  2.00    Male     No  Thur   Lunch     2  0.266312
       232       11.61  3.39    Male     No   Sat  Dinner     2  0.291990
Yes    109       14.31  4.00  Female    Yes   Sat  Dinner     2  0.279525
       183       23.17  6.50    Male    Yes   Sun  Dinner     4  0.280535
       67         3.07  1.00  Female    Yes   Sat  Dinner     1  0.325733
       178        9.60  4.00  Female    Yes   Sun  Dinner     2  0.416667
       172        7.25  5.15    Male    Yes   Sun  Dinner     2  0.710345

top函数在DataFrame的各个片段上调用,然后结果由concat组装到一起,并以分组名称进行了标记。于是,最终结果就有了一个层次换索引,其内层索引值来自原DataFrame`。

如果传给apply的函数能够接受其他参数或关键字,则可以将这些内容放在函数名后面一并传入:

>>> tips.groupby('smoker').apply(top,n=1,column='total_bill')
            total_bill   tip   sex smoker  day    time  size   tip_pct
smoker                                                                
No     212       48.33   9.0  Male     No  Sat  Dinner     4  0.186220
Yes    170       50.81  10.0  Male    Yes  Sat  Dinner     3  0.196812

禁止分组键

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

>>> tips.groupby('smoker',group_keys=False).apply(top)
     total_bill   tip     sex smoker   day    time  size   tip_pct
88        24.71  5.85    Male     No  Thur   Lunch     2  0.236746
185       20.69  5.00    Male     No   Sun  Dinner     5  0.241663
51        10.29  2.60  Female     No   Sun  Dinner     2  0.252672
149        7.51  2.00    Male     No  Thur   Lunch     2  0.266312
232       11.61  3.39    Male     No   Sat  Dinner     2  0.291990
109       14.31  4.00  Female    Yes   Sat  Dinner     2  0.279525
183       23.17  6.50    Male    Yes   Sun  Dinner     4  0.280535
67         3.07  1.00  Female    Yes   Sat  Dinner     1  0.325733
178        9.60  4.00  Female    Yes   Sun  Dinner     2  0.416667
172        7.25  5.15    Male    Yes   Sun  Dinner     2  0.710345

透视表

透视表(pivot table)是各种电子表格程序和其他数据分析软件中一种常见的数据汇总工具。它根据一个或多个键对数据进行聚合,并根据行和列上的分组键将数据分配到各个矩形区域。DataFrame有一个pivot_table方法,此外还有一个顶级的pandas.pivot_table函数。除能为groupby提供便利之外,pivot_table还可以添加分项小计(也叫做margins)。

回到消费数据集,假设我想要根据sexsmoker计算分组平均数(pivot_table的默认聚合类型是计算平均数,想要修改可以修改参数aggfunc=‘mean’),并将sexsmoker放到行上:

>>> tips.pivot_table(index=['sex','smoker'])    # 老版本为rows新版本改为index
                   size       tip   tip_pct  total_bill
sex    smoker                                          
Female No      2.592593  2.773519  0.156921   18.105185
       Yes     2.242424  2.931515  0.182150   17.977879
Male   No      2.711340  3.113402  0.160669   19.791237
       Yes     2.500000  3.051167  0.152771   22.284500

这对groupby来说也是很简单的事。

>>> tips.groupby(['sex','smoker']).mean()   # 一样的效果
               total_bill       tip      size   tip_pct
sex    smoker                                          
Female No       18.105185  2.773519  2.592593  0.156921
       Yes      17.977879  2.931515  2.242424  0.182150
Male   No       19.791237  3.113402  2.711340  0.160669
       Yes      22.284500  3.051167  2.500000  0.152771

现在,假设我们只想聚合tips_pctsize,而且想根据day进行分组。将smoker放到列上,把day放到行上:

>>> tips.pivot_table(['tip_pct','size'],index=['sex','day'],columns=['smoker'])
              tip_pct                size          
smoker             No       Yes        No       Yes
sex    day                                         
Female Fri   0.165296  0.209129  2.500000  2.000000
       Sat   0.147993  0.163817  2.307692  2.200000
       Sun   0.165710  0.237075  3.071429  2.500000
       Thur  0.155971  0.163073  2.480000  2.428571
Male   Fri   0.138005  0.144730  2.000000  2.125000
       Sat   0.162132  0.139067  2.656250  2.629630
       Sun   0.158291  0.173964  2.883721  2.600000
       Thur  0.165706  0.164417  2.500000  2.300000

但是如果想要利用groupby来实现此效果就不能行得通了。

还可以对这个表作进一步处理,传入margins-True添加分项小计。这将会添加标签为All的行和列,其值对应于单个等级中所有数据的分组统计。在下面这个例子中,All值为平均数:不单独考虑烟民与非烟民(All列),不单独考虑行分组两个级别中的任何单项(All行):

>>> tips.pivot_table(['tip_pct','size'],index=['sex','day'],columns='smoker',margins=True)
              tip_pct                          size                    
smoker             No       Yes       All        No       Yes       All
sex    day                                                             
Female Fri   0.165296  0.209129  0.199388  2.500000  2.000000  2.111111
       Sat   0.147993  0.163817  0.156470  2.307692  2.200000  2.250000
       Sun   0.165710  0.237075  0.181569  3.071429  2.500000  2.944444
       Thur  0.155971  0.163073  0.157525  2.480000  2.428571  2.468750
Male   Fri   0.138005  0.144730  0.143385  2.000000  2.125000  2.100000
       Sat   0.162132  0.139067  0.151577  2.656250  2.629630  2.644068
       Sun   0.158291  0.173964  0.162344  2.883721  2.600000  2.810345
       Thur  0.165706  0.164417  0.165276  2.500000  2.300000  2.433333
All          0.159328  0.163196  0.160803  2.668874  2.408602  2.569672

那么这个All列是怎么来的呢?什么叫不单独考虑烟民与非烟民(All列)?我们以第一行的第二列All来进行举例,其实就进行了如下操作:

>>> tips_Fir = tips[tips['day']=='Fri']
>>> tips_result = tips_Fir[tips_Fir['sex']=='Female']
>>> tips_result
     total_bill   tip     sex smoker  day    time  size   tip_pct
92         5.75  1.00  Female    Yes  Fri  Dinner     2  0.173913
93        16.32  4.30  Female    Yes  Fri  Dinner     2  0.263480
94        22.75  3.25  Female     No  Fri  Dinner     2  0.142857
100       11.35  2.50  Female    Yes  Fri  Dinner     2  0.220264
101       15.38  3.00  Female    Yes  Fri  Dinner     2  0.195059
221       13.42  3.48  Female    Yes  Fri   Lunch     2  0.259314
223       15.98  3.00  Female     No  Fri   Lunch     3  0.187735
225       16.27  2.50  Female    Yes  Fri   Lunch     2  0.153657
226       10.09  2.00  Female    Yes  Fri   Lunch     2  0.198216
>>> tips_result['size'].mean()
2.111111111111111

All行道理和列一样,不在进行阐述。

要使用其他的聚合函数,将其传给aggfunc即可。例如,使用countlen可以得到有关分组大小的交叉表:

>>> tips.pivot_table('tip_pct',index=['sex','smoker'],columns='day',aggfunc=len,margins=True)
day             Fri   Sat   Sun  Thur    All
sex    smoker                               
Female No       2.0  13.0  14.0  25.0   54.0
       Yes      7.0  15.0   4.0   7.0   33.0
Male   No       2.0  32.0  43.0  20.0   97.0
       Yes      8.0  27.0  15.0  10.0   60.0
All            19.0  87.0  76.0  62.0  244.0

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

>>> tips.pivot_table('tip_pct',index=['time','sex','smoker'],columns='day',aggfunc='sum',fill_value=0)
day                        Fri       Sat       Sun      Thur
time   sex    smoker                                        
Dinner Female No      0.142857  1.923915  2.319939  0.159744
              Yes     0.852716  2.457251  0.948299  0.000000
       Male   No      0.276010  5.188230  6.806499  0.000000
              Yes     0.635410  3.754804  2.609457  0.000000
Lunch  Female No      0.187735  0.000000  0.000000  3.739542
              Yes     0.611188  0.000000  0.000000  1.141508
       Male   No      0.000000  0.000000  0.000000  3.314127
              Yes     0.522432  0.000000  0.000000  1.644168

pivot_table参数说明。

参数名说明
values待聚合的列的名称。默认聚合所有数值列
indexs用于分组的列明或其他分组键,出现在透视表结果的行
columns用于分组的列明或其他分组键,出现在透视表结果的列
aggfunc聚合函数或函数列表,默认为‘mean’。可以是任何对groupby有效的函数
fill_value用于替换结果表中的缺失值
margins添加行/列小计和总计,默认为False
  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值