1. 程式人生 > >第十篇 資料聚合與分組運算

第十篇 資料聚合與分組運算

對資料集進⾏分組並對各組應⽤⼀個函式(⽆論是聚合還是轉換),通常是資料分析⼯作中的重要環節。在將資料集載入、融合、準備好之後,通常就是計算分組統計或⽣成透視表。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