1. 程式人生 > >資料基礎---《利用Python進行資料分析·第2版》第10章 資料聚合與分組運算

資料基礎---《利用Python進行資料分析·第2版》第10章 資料聚合與分組運算

之前自己對於numpy和pandas是要用的時候東學一點西一點,直到看到《利用Python進行資料分析·第2版》,覺得只看這一篇就夠了。非常感謝原博主的翻譯和分享。

對資料集進行分組並對各組應用一個函式(無論是聚合還是轉換),通常是資料分析工作中的重要環節。在將資料集載入、融合、準備好之後,通常就是計算分組統計或生成透視表。pandas提供了一個靈活高效的gruopby功能,它使你能以一種自然的方式對資料集進行切片、切塊、摘要等操作。

關係型資料庫和SQL(Structured Query Language,結構化查詢語言)能夠如此流行的原因之一就是其能夠方便地對資料進行連線、過濾、轉換和聚合。但是,像SQL這樣的查詢語言所能執行的分組運算的種類很有限。在本章中你將會看到,由於Python和pandas強大的表達能力,我們可以執行復雜得多的分組運算(利用任何可以接受pandas物件或NumPy陣列的函式)。在本章中,你將會學到:

  • 使用一個或多個鍵(形式可以是函式、陣列或DataFrame列名)分割pandas物件。
  • 計算分組的概述統計,比如數量、平均值或標準差,或是使用者定義的函式。
  • 應用組內轉換或其他運算,如規格化、線性迴歸、排名或選取子集等。
  • 計算透視表或交叉表。
  • 執行分位數分析以及其它統計分組分析。

筆記:對時間序列資料的聚合(groupby的特殊用法之一)也稱作重取樣(resampling),本書將在第11章中單獨對其進行講解。

10.1 GroupBy機制

Hadley Wickham(許多熱門R語言包的作者)創造了一個用於表示分組運算的術語"split-apply-combine"(拆分-應用-合併)。第一個階段,pandas物件(無論是Series、DataFrame還是其他的)中的資料會根據你所提供的一個或多個鍵被拆分(split)為多組。拆分操作是在物件的特定軸上執行的。例如,DataFrame可以在其行(axis=0)或列(axis=1)上進行分組。然後,將一個函式應用(apply)到各個分組併產生一個新值。最後,所有這些函式的執行結果會被合併(combine)到最終的結果物件中。結果物件的形式一般取決於資料上所執行的操作。圖10-1大致說明了一個簡單的分組聚合過程。

圖10-1 分組聚合演示

分組鍵可以有多種形式,且型別不必相同:

  • 列表或陣列,其長度與待分組的軸一樣。
  • 表示DataFrame某個列名的值。
  • 字典或Series,給出待分組軸上的值與分組名之間的對應關係。
  • 函式,用於處理軸索引或索引中的各個標籤。

注意,後三種都只是快捷方式而已,其最終目的仍然是產生一組用於拆分物件的值。如果覺得這些東西看起來很抽象,不用擔心,我將在本章中給出大量有關於此的示例。首先來看看下面這個非常簡單的表格型資料集(以DataFrame的形式):

import pandas as pd
import numpy as
np df=pd.DataFrame({'key1':['a', 'a', 'b', 'b', 'a'],'key2': ['one', 'two', 'one', 'two', 'one'],'data1':np.random.randn(5),'data2':np.random.randn(5)}) df
data1 data2 key1 key2
0 0.085071 -0.681321 a one
1 -0.320136 -1.545958 a two
2 0.692366 0.697484 b one
3 0.442214 1.022998 b two
4 -0.990618 0.514342 a one

假設你想要按key1進行分組,並計算data1列的平均值。實現該功能的方式有很多,而我們這裡要用的是:訪問data1,並根據key1呼叫groupby:

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

變數grouped是一個GroupBy物件。它實際上還沒有進行任何計算,只是含有一些有關分組鍵df[‘key1’]的中間資料而已。換句話說,該物件已經有了接下來對各分組執行運算所需的一切資訊。例如,我們可以呼叫GroupBy的mean方法來計算分組平均值:

grouped.mean()
key1
a   -0.408561
b    0.567290
Name: data1, dtype: float64

稍後我將詳細講解.mean()的呼叫過程。這裡最重要的是,資料(Series)根據分組鍵進行了聚合,產生了一個新的Series,其索引為key1列中的唯一值。之所以結果中索引的名稱為key1,是因為原始DataFrame的列df[‘key1’]就叫這個名字。

如果我們一次傳入多個數組的列表,就會得到不同的結果:

means = df['data1'].groupby([df['key1'],df['key2']]).mean()
means
key1  key2
a     one    -0.452774
      two    -0.320136
b     one     0.692366
      two     0.442214
Name: data1, dtype: float64

這裡,我通過兩個鍵對資料進行了分組,得到的Series具有一個層次化索引(由唯一的鍵對組成):

means.unstack()
key2 one two
key1
a -0.452774 -0.320136
b 0.692366 0.442214

在這個例子中,分組鍵均為Series。實際上,分組鍵可以是任何長度適當的陣列:這裡可以看作是通用形式

states=np.array(['Ohio', 'California', 'California', 'Ohio', 'Ohio'])
years=np.array([2005, 2005, 2006, 2005, 2006])
df['data1'].groupby([states,years]).mean()
California  2005   -0.320136
            2006    0.692366
Ohio        2005    0.263643
            2006   -0.990618
Name: data1, dtype: float64

通常,分組資訊就位於相同的要處理DataFrame中。這裡,你還可以將列名(可以是字串、數字或其他Python物件)用作分組鍵:這裡可看作是特殊形式

df.groupby('key1').mean()
data1 data2
key1
a -0.408561 -0.570979
b 0.567290 0.860241
df.groupby(['key1','key2']).mean()
data1 data2
key1 key2
a one -0.452774 -0.083489
two -0.320136 -1.545958
b one 0.692366 0.697484
two 0.442214 1.022998

你可能已經注意到了,第一個例子在執行df.groupby(‘key1’).mean()時,結果中沒有key2列。這是因為df[‘key2’]不是數值資料(俗稱“麻煩列”),所以被從結果中排除了。預設情況下,所有數值列都會被聚合,雖然有時可能會被過濾為一個子集,稍後就會碰到。

無論你準備拿groupby做什麼,都有可能會用到GroupBy的size方法(可以看作是apply後的特殊聚合函式),它可以返回一個含有分組大小的Series:

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

注意,任何分組關鍵詞中的缺失值,都會被從結果中除去。

對分組進行迭代

GroupBy物件支援迭代,可以產生一組二元元組(由分組名和資料塊組成)。看下面的例子:

for name,group in df.groupby('key1'):
    print(name)
    print(group)
a
      data1     data2 key1 key2
0  0.085071 -0.681321    a  one
1 -0.320136 -1.545958    a  two
4 -0.990618  0.514342    a  one
b
      data1     data2 key1 key2
2  0.692366  0.697484    b  one
3  0.442214  1.022998    b  two

對於多重鍵的情況,元組的第一個元素將會是由鍵值組成的元組:

for (k1,k2),group in df.groupby(['key1', 'key2']):
    print((k1,k2))
    print(group)
('a', 'one')
      data1     data2 key1 key2
0  0.085071 -0.681321    a  one
4 -0.990618  0.514342    a  one
('a', 'two')
      data1     data2 key1 key2
1 -0.320136 -1.545958    a  two
('b', 'one')
      data1     data2 key1 key2
2  0.692366  0.697484    b  one
('b', 'two')
      data1     data2 key1 key2
3  0.442214  1.022998    b  two

當然,你可以對這些資料片段做任何操作。有一個你可能會覺得有用的運算:將這些資料片段做成一個字典:

list(df.groupby('key1'))
[('a',       data1     data2 key1 key2
  0  0.085071 -0.681321    a  one
  1 -0.320136 -1.545958    a  two
  4 -0.990618  0.514342    a  one), ('b',       data1     data2 key1 key2
  2  0.692366  0.697484    b  one
  3  0.442214  1.022998    b  two)]
pieces = dict(list(df.groupby('key1')))
pieces
{'a':       data1     data2 key1 key2
 0  0.085071 -0.681321    a  one
 1 -0.320136 -1.545958    a  two
 4 -0.990618  0.514342    a  one, 'b':       data1     data2 key1 key2
 2  0.692366  0.697484    b  one
 3  0.442214  1.022998    b  two}

groupby預設是在axis=0上進行分組的,通過設定也可以在其他任何軸上進行分組。拿上面例子中的df來說,我們可以根據dtype對列進行分組:

df.dtypes
data1    float64
data2    float64
key1      object
key2      object
dtype: object
grouped=df.groupby(df.dtypes,axis=1)

可以如下列印分組:

for dtype, group in grouped:
    print(dtype)
    print(group)
float64
      data1     data2
0  0.085071 -0.681321
1 -0.320136 -1.545958
2  0.692366  0.697484
3  0.442214  1.022998
4 -0.990618  0.514342
object
  key1 key2
0    a  one
1    a  two
2    b  one
3    b  two
4    a  one

選取一列或列的子集

對於由DataFrame產生的GroupBy物件,如果用一個(單個字串)或一組(字串陣列)列名對其進行索引,就能實現選取部分列進行聚合的目的。也就是說:
從下面也可以看到,df[‘data1’]會得到SeriesGroupBy 物件,而df[[‘data2’]]會得到DataFrameGroupBy 物件。

df.groupby('key1')['data1']
<pandas.core.groupby.groupby.SeriesGroupBy object at 0x000001105F09A198>
df.groupby('key1')[['data2']]
<pandas.core.groupby.groupby.DataFrameGroupBy object at 0x000001105F09AB38>

是以下程式碼的語法糖:

df['data1'].groupby(df['key1'])
df[['data2']].groupby(df['key1'])
<pandas.core.groupby.groupby.DataFrameGroupBy object at 0x000001105F09AC18>

尤其對於大資料集,很可能只需要對部分列進行聚合。例如,在前面那個資料集中,如果只需計算data2列的平均值並以DataFrame形式得到結果,可以這樣寫:

df.groupby(['key1', 'key2'])[['data2']].mean()
data2
key1 key2
a one -0.083489
two -1.545958
b one 0.697484
two 1.022998

這種索引操作所返回的物件是一個已分組的DataFrame(如果傳入的是列表或陣列)或已分組的Series(如果傳入的是標量形式的單個列名):

s_grouped=df.groupby(['key1', 'key2'])['data2']
s_grouped
<pandas.core.groupby.groupby.SeriesGroupBy object at 0x000001105F09AD30>
s_grouped.mean()
key1  key2
a     one    -0.083489
      two    -1.545958
b     one     0.697484
      two     1.022998
Name: data2, dtype: float64

通過字典或Series進行分組

除陣列以外,分組資訊還可以其他形式存在,道理是一樣的。來看另一個示例DataFrame:

people = pd.DataFrame(np.random.randn(5, 5),columns=['a', 'b', 'c', 'd', 'e'],index=['Joe', 'Steve', 'Wes', 'Jim', 'Travis'])
people.iloc[2:3,[1,2]]=np.nan
people
a b c d e
Joe -1.816206 0.524415 1.445611 0.363604 -0.611044
Steve 0.096662 2.645132 0.657724 0.536647 -0.945192
Wes 0.075040 NaN NaN -2.047711 -0.903662
Jim -0.377932 -0.608861 -1.419598 -0.820359 -2.075691
Travis -0.290854 -0.252215 -0.823395 2.405941 -0.030716

現在,假設已知列的分組關係,並希望根據分組計算列的和:

mapping = {'a': 'red', 'b': 'red', 'c': 'blue', 'd': 'blue', 'e': 'red', 'f' : 'orange'}
by_column =people.groupby(mapping,axis=1)
for name,group in by_column:
    print(name)
    print(group)
blue
               c         d
Joe     1.445611  0.363604
Steve   0.657724  0.536647
Wes          NaN -2.047711
Jim    -1.419598 -0.820359
Travis -0.823395  2.405941
red
               a         b         e
Joe    -1.816206  0.524415 -0.611044
Steve   0.096662  2.645132 -0.945192
Wes     0.075040       NaN -0.903662
Jim    -0.377932 -0.608861 -2.075691
Travis -0.290854 -0.252215 -0.030716
by_column.sum()
blue red
Joe 1.809215 -1.902835
Steve 1.194371 1.796602
Wes -2.047711 -0.828622
Jim -2.239957 -3.062484
Travis 1.582547 -0.573784

Series也有同樣的功能,它可以被看做一個固定大小的對映

map_series =pd.Series(mapping)
map_series
a       red
b       red
c      blue
d      blue
e       red
f    orange
dtype: object
people.groupby(map_series,axis=1).count()
blue red
Joe 2 3
Steve 2 3
Wes 1 2
Jim 2 3
Travis 2 3

通過函式進行分組

比起使用字典或Series,使用Python函式是一種更原生的方法定義分組對映。任何被當做分組鍵的函式都會在各個索引值上被呼叫一次,其返回值就會被用作分組名稱。具體點說,以上一小節的示例DataFrame為例,其索引值為人的名字。你可以計算一個字串長度的陣列,更簡單的方法是傳入len函式:

people.groupby(len).sum()#len操作的是索引
a b c d e
3 -2.119099 -0.084446 0.026014 -2.504467 -3.590397
5 0.096662 2.645132 0.657724 0.536647 -0.945192
6 -0.290854 -0.252215 -0.823395 2.405941 -0.030716

將函式跟陣列、列表、字典、Series混合使用也不是問題,因為任何東西在內部都會被轉換為陣列

key_list = ['one', 'one', 'one', 'two', 'two']
people.groupby([len,key_list]).min()
a b c d e
3 one -1.816206 0.524415 1.445611 -2.047711 -0.903662
two -0.377932 -0.608861 -1.419598 -0.820359 -2.075691
5 one 0.096662 2.645132 0.657724 0.536647 -0.945192
6 two -0.290854 -0.252215 -0.823395 2.405941 -0.030716

根據索引級別分組

層次化索引資料集最方便的地方就在於它能夠根據軸索引的一個級別進行聚合:

columns=pd.MultiIndex.from_arrays([['US', 'US', 'US', 'JP', 'JP'],[1, 3, 5, 1, 3]],names=['cty','tenor'])
columns
MultiIndex(levels=[['JP', 'US'], [1, 3, 5]],
           labels=[[1, 1, 1, 0, 0], [0, 1, 2, 0, 1]],
           names=['cty', 'tenor'])
hier_df = pd.DataFrame(np.random.randn(4,5),columns=columns)
hier_df
cty US JP
tenor 1 3 5 1 3
0 1.916257 1.689067 -0.518793 -0.390765 -0.863206
1 0.473669 0.544845 1.649351 0.841178 0.379657
2 0.957656 0.361352 -1.817090 -0.376896 -0.073625
3 0.619413 -0.051054 1.425500 -0.807275 1.486081

要根據級別分組,使用level關鍵字傳遞級別序號或名字:

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

10.2 資料聚合

聚合指的是任何能夠從陣列產生標量值的資料轉換過程。之前的例子已經用過一些,比如mean、count、min以及sum等。你可能想知道在GroupBy物件上呼叫mean()時究竟發生了什麼。許多常見的聚合運算(如表10-1所示)都有進行優化。然而,除了這些方法,你還可以使用其它的。

函式名 說明
count 分組中非NA值的數量
sum 非NA值的和
mean 非NA值的平均值
median 非NA值的算術中位數
std、var 無偏(分母為n-1)標準差和方差
min、max 非NA值的最小值和最大值
prod 非NA值的積
first、last 第一個和最後一個非NA值
表10-1 經過優化的groupby方法
你可以使用自己發明的聚合運算,還可以呼叫分組物件上已經定義好的任何方法。例如,quantile可以計算Series或DataFrame列的樣本分位數。

雖然quantile並沒有明確地實現於GroupBy,但它是一個Series方法,所以這裡是能用的。實際上,GroupBy會高效地對Series進行切片,然後對各片呼叫piece.quantile(0.9),最後將這些結果組裝成最終結果:

df
data1 data2 key1 key2
0 0.085071 -0.681321 a one
1 -0.320136 -1.545958 a two
2 0.692366 0.697484 b one
3 0.442214 1.022998 b two
4 -0.990618 0.514342 a one
grouped=df.groupby('key1')
grouped['data1'].quantile(0.9)
key1
a    0.004030
b    0.667351
Name: data1, dtype: float64

如果要使用你自己的聚合函式,只需將其傳入aggregate或agg方法即可:

def peak_to_peak(arr):
    return arr.max()-arr.min()
grouped.agg(peak_to_peak)
data1 data2
key1
a 1.075689 2.060300
b 0.250152 0.325513

你可能注意到注意,有些方法(如describe)也是可以用在這裡的,即使嚴格來講,它們並非聚合運算(其實也是聚合,只不過一下子完成了多種聚合):

grouped.describe()
data1 data2
count mean std min 25% 50% 75% max count mean std min 25% 50% 75% max
key1
a 3.0 -0.408561 0.543269 -0.990618 -0.655377 -0.320136 -0.117532 0.085071 3.0 -0.570979 1.034573 -1.545958 -1.113639 -0.681321 -0.083489 0.514342
b 2.0 0.567290 0.176884 0.442214 0.504752 0.567290 0.629828 0.692366 2.0 0.860241 0.230173 0.697484 0.778863 0.860241 0.941619 1.022998

在後面的10.3節,我將詳細說明這到底是怎麼回事。

筆記:自定義聚合函式要比表10-1中那些經過優化的函式慢得多。這是因為在構造中間分組資料塊時存在非常大的開銷(函式呼叫、資料重排等)。

面向列的多函式應用

回到前面小費的例子。使用read_csv匯入資料之後,我們添加了一個小費百分比的列tip_pct:

tips = pd.read_csv('examples/tips.csv')
# Add tip percentage of total bill
tips['tip_pct']=tips['tip']/tips['total_bill']
tips.head(5)
total_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

你已經看到,對Series或DataFrame列的聚合運算其實就是使用aggregate(使用自定義函式)或呼叫諸如mean、std之類的方法。然而,你可能希望對不同的列使用不同的聚合函式,或一次應用多個函式。其實這也好辦,我將通過一些示例來進行講解。首先,我根據天和smoker對tips進行分組:

grouped=tips.groupby(['smoker','day'])

注意,對於表10-1中的那些描述統計,可以將函式名以字串的形式傳入:

grouped_pct = grouped['tip_pct']
grouped_pct.agg('mean')
smoker  day 
No      Fri     0.151650
        Sat     0.158048
        Sun     0.160113
        Thur    0.160298
Yes     Fri     0.174783
        Sat     0.147906
        Sun     0.187250
        Thur    0.163863
Name: tip_pct, dtype: float64

如果傳入一組函式或函式名,得到的DataFrame的列就會以相應的函式命名:

grouped_pct.agg(['mean','std',peak_to_peak])
mean std peak_to_peak
smoker day
No Fri 0.151650 0.028123 0.067349
Sat 0.158048 0.039767 0.235193
Sun 0.160113 0.042347 0.193226
Thur 0.160298 0.038774 0.193350
Yes Fri 0.174783 0.051293 0.159925
Sat 0.147906 0.061375 0.290095
Sun 0.187250 0.154134 0.644685
Thur 0.163863 0.039389 0.151240

這裡,我們傳遞了一組聚合函式進行聚合,獨立對資料分組進行評估。

你並非一定要接受GroupBy自動給出的那些列名,特別是lambda函式,它們的名稱是’’,這樣的辨識度就很低了(通過函式的name屬性看看就知道了)。因此,如果傳入的是一個由(name,function)元組組成的列表,則各元組的第一個元素就會被用作DataFrame的列名(可以將這種二元元組列表看做一個有序對映):

grouped_pct.agg([('foo','mean'),('bar','std')])
foo bar
smoker day
No Fri 0.151650 0.028123
Sat 0.158048 0.039767
Sun 0.160113 0.042347
Thur 0.160298 0.038774
Yes Fri 0.174783 0.051293
Sat 0.147906 0.061375
Sun 0.187250 0.154134
Thur 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
smoker day
No Fri 4 0.151650 0.187735 4 18.420000 22.75
Sat 45 0.158048 0.291990 45 19.661778 48.33
Sun 57 0.160113 0.252672 57 20.506667 48.17
Thur 45 0.160298 0.266312 45 17.113111 41.19
Yes Fri 15 0.174783 0.263480 15 16.813333 40.17
Sat 42 0.147906 0.325733 42 21.276667 50.81
Sun 19 0.187250 0.710345 19 24.120000 45.35
Thur 17 0.163863 0.241255 17 19.190588 43.11

如你所見,結果DataFrame擁有層次化的列,這相當於分別對各列進行聚合,然後用concat將結果組裝到一起,使用列名用作keys引數:

result['tip_pct']
count mean max
smoker day
No Fri 4 0.151650 0.187735
Sat 45 0.158048 0.291990
Sun 57 0.160113 0.252672
Thur 45 0.160298 0.266312
Yes Fri 15 0.174783 0.263480
Sat 42 0.147906 0.325733
Sun 19 0.187250 0.710345
Thur 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
smoker day
No Fri 0.151650 0.000791 18.420000 25.596333
Sat 0.158048 0.001581 19.661778 79.908965
Sun 0.160113 0.001793 20.506667 66.099980
Thur 0.160298 0.001503 17.113111 59.625081
Yes Fri 0.174783 0.002631 16.813333 82.562438
Sat 0.147906 0.003767 21.276667 101.387535
Sun 0.187250 0.023757 24.120000 109.046044
Thur 0.163863 0.001551 19.190588 69.808518

現在,假設你想要對一個列或不同的列應用不同的函式。具體的辦法是向agg傳入一個從列名對映到函式的字典:

grouped.agg({'tip':np.max,'size':'sum'}