第十篇 資料聚合與分組運算
對資料集進⾏分組並對各組應⽤⼀個函式(⽆論是聚合還是轉換),通常是資料分析⼯作中的重要環節。在將資料集載入、融合、準備好之後,通常就是計算分組統計或⽣成透視表。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⼤致說明了⼀個簡單的分組聚合過程。
分組鍵可以有多種形式,且型別不必相同:
列表或陣列,其⻓度與待分組的軸⼀樣。
表示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所示)都有進⾏優化。然⽽,除了這些⽅法,你還可以使⽤其它的。
你可以使⽤⾃⼰發明的聚合運算,還可以調⽤分組物件上已經定義好的任何⽅法。例如,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會將待處理的物件拆分成多個⽚段,然後對各⽚段調⽤傳⼊的函式,最後嘗試將各⽚段組合到⼀起。
圖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