對數據集進行分組並對各組應用一個函數(無論是聚合還是轉換),通常是數據分析工作中的重要環節。在將數據集加載、融合、准備好之后,通常就是計算分組統計或生成透視表。pandas提供了一個靈活高效的gruopby功能,它使你能以一種自然的方式對數據集進行切片、切塊、摘要等操作。
關系型數據庫和SQL(Structured Query Language,結構化查詢語言)能夠如此流行的原因之一就是其能夠方便地對數據進行連接、過濾、轉換和聚合。但是,像SQL這樣的查詢語言所能執行的分組運算的種類很有限。在本章中你將會看到,由於Python和pandas強大的表達能力,我們可以執行復雜得多的分組運算(利用任何可以接受pandas對象或NumPy數組的函數)。在本章中,你將會學到:
- 使用一個或多個鍵(形式可以是函數、數組或DataFrame列名)分割pandas對象。
- 計算分組的概述統計,比如數量、平均值或標准差,或是用戶定義的函數。
- 應用組內轉換或其他運算,如規格化、線性回歸、排名或選取子集等。
- 計算透視表或交叉表。
- 執行分位數分析以及其它統計分組分析。
筆記:對時間序列數據的聚合(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的形式):
import pandas as pd
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 | 1.318468 | 0.764612 |
1 | a | two | -0.670063 | 1.056639 |
2 | b | one | -2.405182 | 0.665323 |
3 | b | two | 0.734192 | 0.436943 |
4 | a | one | -0.591552 | 0.523801 |
假設你想要按key1進行分組,並計算data1列的平均值。實現該功能的方式有很多,而我們這里要用的是:訪問data1,並根據key1調用groupby:
grouped = df['data1'].groupby(df['key1'])
grouped
<pandas.core.groupby.groupby.SeriesGroupBy object at 0x7fafcbdec4a8>
變量grouped是一個GroupBy對象。它實際上還沒有進行任何計算,只是含有一些有關分組鍵df[‘key1’]的中間數據而已。換句話說,該對象已經有了接下來對各分組執行運算所需的一切信息。例如,我們可以調用GroupBy的mean方法來計算分組平均值
grouped.mean()
key1
a 0.018951
b -0.835495
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.363458
two -0.670063
b one -2.405182
two 0.734192
Name: data1, dtype: float64
這里,我通過兩個鍵對數據進行了分組,得到的Series具有一個層次化索引(由唯一的鍵對組成):
means.unstack()
key2 | one | two |
---|---|---|
key1 | ||
a | 0.363458 | -0.670063 |
b | -2.405182 | 0.734192 |
在這個例子中,分組鍵均為Series。實際上,分組鍵可以是任何長度適當的數組:
states = np.array(['Ohio', 'California', 'California', 'Ohio', 'Ohio'])
years = np.array([2005, 2005, 2006, 2005, 2006])
df['data1']
0 1.318468
1 -0.670063
2 -2.405182
3 0.734192
4 -0.591552
Name: data1, dtype: float64
df['data1'].groupby([states, years]).mean()
California 2005 -0.670063
2006 -2.405182
Ohio 2005 1.026330
2006 -0.591552
Name: data1, dtype: float64
通常,分組信息就位於相同的要處理DataFrame中。這里,你還可以將列名(可以是字符串、數字或其他Python對象)用作分組鍵:
df.groupby('key1').mean()
data1 | data2 | |
---|---|---|
key1 | ||
a | 0.018951 | 0.781684 |
b | -0.835495 | 0.551133 |
df.groupby(['key1', 'key2']).mean()
data1 | data2 | ||
---|---|---|---|
key1 | key2 | ||
a | one | 0.363458 | 0.644206 |
two | -0.670063 | 1.056639 | |
b | one | -2.405182 | 0.665323 |
two | 0.734192 | 0.436943 |
你可能已經注意到了,第一個例子在執行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
注意,任何分組關鍵詞中的缺失值,都會被從結果中除去。
對分組進行迭代
GroupBy對象支持迭代,可以產生一組二元元組(由分組名和數據塊組成)。看下面的例子:
for name, group in df.groupby('key1'):
print(name)
print(group)
a
key1 key2 data1 data2
0 a one 1.318468 0.764612
1 a two -0.670063 1.056639
4 a one -0.591552 0.523801
b
key1 key2 data1 data2
2 b one -2.405182 0.665323
3 b two 0.734192 0.436943
對於多重鍵的情況,元組的第一個元素將會是由鍵值組成的元組:
for (k1, k2), group in df.groupby(['key1', 'key2']):
print((k1, k2))
print(group)
('a', 'one')
key1 key2 data1 data2
0 a one 1.318468 0.764612
4 a one -0.591552 0.523801
('a', 'two')
key1 key2 data1 data2
1 a two -0.670063 1.056639
('b', 'one')
key1 key2 data1 data2
2 b one -2.405182 0.665323
('b', 'two')
key1 key2 data1 data2
3 b two 0.734192 0.436943
當然,你可以對這些數據片段做任何操作。有一個你可能會覺得有用的運算:將這些數據片段做成一個字典:
pieces = dict(list(df.groupby('key1')))
pieces['b']
key1 | key2 | data1 | data2 | |
---|---|---|---|---|
2 | b | one | -2.405182 | 0.665323 |
3 | b | two | 0.734192 | 0.436943 |
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 1.318468 0.764612
1 -0.670063 1.056639
2 -2.405182 0.665323
3 0.734192 0.436943
4 -0.591552 0.523801
object
key1 key2
0 a one
1 a two
2 b one
3 b two
4 a one
選取一列或列的子集
對於由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.644206 |
two | 1.056639 | |
b | one | 0.665323 |
two | 0.436943 |
這種索引操作所返回的對象是一個已分組的DataFrame(如果傳入的是列表或數組)或已分組的Series(如果傳入的是標量形式的單個列名):
s_grouped = df.groupby(['key1', 'key2'])['data2']
s_grouped
<pandas.core.groupby.groupby.SeriesGroupBy object at 0x7faffdef6550>
s_grouped.mean()
key1 key2
a one 0.644206
two 1.056639
b one 0.665323
two 0.436943
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
a | b | c | d | e | |
---|---|---|---|---|---|
Joe | 1.037806 | 0.366019 | -1.868240 | -1.574181 | 1.229462 |
Steve | -0.537422 | -0.149428 | 1.065657 | 1.193845 | 1.381285 |
Wes | -0.120145 | -1.216974 | 0.690470 | 0.676304 | -1.032362 |
Jim | -0.071084 | 1.278099 | -0.060597 | -0.354461 | -0.118191 |
Travis | -0.285226 | -0.894874 | 0.169473 | 1.330677 | 0.586171 |
people.iloc[2:3, [1, 2]] = np.nan # Add a few NA values
people
a | b | c | d | e | |
---|---|---|---|---|---|
Joe | 1.037806 | 0.366019 | -1.868240 | -1.574181 | 1.229462 |
Steve | -0.537422 | -0.149428 | 1.065657 | 1.193845 | 1.381285 |
Wes | -0.120145 | NaN | NaN | 0.676304 | -1.032362 |
Jim | -0.071084 | 1.278099 | -0.060597 | -0.354461 | -0.118191 |
Travis | -0.285226 | -0.894874 | 0.169473 | 1.330677 | 0.586171 |
現在,假設已知列的分組關系,並希望根據分組計算列的和:
mapping = {'a': 'red', 'b': 'red', 'c': 'blue',
'd': 'blue', 'e': 'red', 'f' : 'orange'}
現在,你可以將這個字典傳給groupby,來構造數組,但我們可以直接傳遞字典(我包含了鍵“f”來強調,存在未使用的分組鍵是可以的):
by_column = people.groupby(mapping, axis=1)
by_column
<pandas.core.groupby.groupby.DataFrameGroupBy object at 0x7faffdea94e0>
by_column.sum()
blue | red | |
---|---|---|
Joe | -3.442421 | 2.633287 |
Steve | 2.259502 | 0.694435 |
Wes | 0.676304 | -1.152507 |
Jim | -0.415058 | 1.088824 |
Travis | 1.500150 | -0.593930 |
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()
a | b | c | d | e | |
---|---|---|---|---|---|
3 | 0.846577 | 1.644117 | -1.928837 | -1.252338 | 0.078909 |
5 | -0.537422 | -0.149428 | 1.065657 | 1.193845 | 1.381285 |
6 | -0.285226 | -0.894874 | 0.169473 | 1.330677 | 0.586171 |
將函數跟數組、列表、字典、Series混合使用也不是問題,因為任何東西在內部都會被轉換為數組:
key_list = ['one', 'one', 'one', 'two', 'two']
people.groupby([len, key_list]).min()
a | b | c | d | e | ||
---|---|---|---|---|---|---|
3 | one | -0.120145 | 0.366019 | -1.868240 | -1.574181 | -1.032362 |
two | -0.071084 | 1.278099 | -0.060597 | -0.354461 | -0.118191 | |
5 | one | -0.537422 | -0.149428 | 1.065657 | 1.193845 | 1.381285 |
6 | two | -0.285226 | -0.894874 | 0.169473 | 1.330677 | 0.586171 |
根據索引級別分組
層次化索引數據集最方便的地方就在於它能夠根據軸索引的一個級別進行聚合:
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 | 2.503014 | -0.354419 | -0.911664 | -2.230766 | 0.214306 |
1 | -1.780458 | -2.004709 | -0.321290 | -0.505333 | 0.130555 |
2 | 1.017626 | -0.728401 | -0.385364 | -0.603360 | -1.053275 |
3 | -0.016153 | 0.906594 | 1.225777 | 0.872585 | 0.931181 |
要根據級別分組,使用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所示)都有進行優化。然而,除了這些方法,你還可以使用其它的。
函數名 | 說明 |
---|---|
count | 分組中的NA值的數量 |
sum | 非NA值的和 |
mean | 非NA值的平均值 |
median | 非NA值的算術中位數 |
std,var | 無偏(分母為n-1)標准差和方差 |
min,max | 非NA值的最小值和最大值 |
prod | 非NA值的積 |
first,last | 第一個和最后一個非NA值 |
你可以使用自己發明的聚合運算,還可以調用分組對象上已經定義好的任何方法。例如,quantile可以計算Series或DataFrame列的樣本分位數。
雖然quantile並沒有明確地實現於GroupBy,但它是一個Series方法,所以這里是能用的。實際上,GroupBy會高效地對Series進行切片,然后對各片調用piece.quantile(0.9),最后將這些結果組裝成最終結果:
df
key1 | key2 | data1 | data2 | |
---|---|---|---|---|
0 | a | one | 1.318468 | 0.764612 |
1 | a | two | -0.670063 | 1.056639 |
2 | b | one | -2.405182 | 0.665323 |
3 | b | two | 0.734192 | 0.436943 |
4 | a | one | -0.591552 | 0.523801 |
grouped = df.groupby('key1')
grouped['data1'].quantile(0.9)
key1
a 0.936464
b 0.420254
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.988531 | 0.532838 |
b | 3.139374 | 0.228380 |
你可能注意到注意,有些方法(如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.018951 | 1.126099 | -0.670063 | -0.630807 | -0.591552 | 0.363458 | 1.318468 | 3.0 | 0.781684 | 0.266829 | 0.523801 | 0.644206 | 0.764612 | 0.910625 | 1.056639 |
b | 2.0 | -0.835495 | 2.219873 | -2.405182 | -1.620339 | -0.835495 | -0.050652 | 0.734192 | 2.0 | 0.551133 | 0.161489 | 0.436943 | 0.494038 | 0.551133 | 0.608228 | 0.665323 |
在后面的10.3節,我將詳細說明這到底是怎么回事。
筆記:自定義聚合函數要比表10-1中那些經過優化的函數慢得多。這是因為在構造中間分組數據塊時存在非常大的開銷(函數調用、數據重排等)。
面向列的多函數應用
回到前面小費的例子。使用read_csv導入數據之后,我們添加了一個小費百分比的列tip_pct:
tips = pd.read_csv('examples/tips.csv')
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之類的方法。然而,你可能希望對不同的列使用不同的聚合函數,或一次應用多個函數。其實這也好辦,我將通過一些示例來進行講解。首先,我根據天和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函數,它們的名稱是’’,這樣的辨識度就很低了(通過函數的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才會擁有層次化的列。
以“沒有行索引”的形式返回聚合數據
到目前為止,所有示例中的聚合數據都有由唯一的分組鍵組成的索引(可能還是層次化的)。由於並不總是需要如此,所以你可以向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會將待處理的對象拆分成多個片段,然后對各片段調用傳入的函數,最后嘗試將各片段組合到一起。
回到之前那個小費數據集,假設你想要根據分組選出最高的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 | 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% | max | |
---|---|---|---|---|---|---|---|---|
smoker | ||||||||
No | 151.0 | 0.159328 | 0.039910 | 0.056797 | 0.136906 | 0.155625 | 0.185014 | 0.291990 |
Yes | 93.0 | 0.163196 | 0.085119 | 0.035638 | 0.106771 | 0.153846 | 0.195059 | 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)
禁止分組鍵
從上面的例子中可以看出,分組鍵會跟原始對象的索引共同構成結果對象中的層次化索引。將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 |
分位數和桶分析
我曾在第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)
quartiles[:10]
0 (0.111, 1.769]
1 (-1.547, 0.111]
2 (0.111, 1.769]
3 (-1.547, 0.111]
4 (-1.547, 0.111]
5 (-1.547, 0.111]
6 (-1.547, 0.111]
7 (0.111, 1.769]
8 (-1.547, 0.111]
9 (-1.547, 0.111]
Name: data1, dtype: category
Categories (4, interval[float64]): [(-3.212, -1.547] < (-1.547, 0.111] < (0.111, 1.769] < (1.769, 3.427]]
由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)
grouped.apply(get_stats)
data1
(-3.212, -1.547] count 61.000000
max 1.681975
mean -0.034289
min -2.751665
(-1.547, 0.111] count 493.000000
max 3.286570
mean -0.115413
min -3.273816
(0.111, 1.769] count 406.000000
max 3.832312
mean -0.017925
min -3.711830
(1.769, 3.427] count 40.000000
max 2.301913
mean 0.029012
min -3.133572
Name: data2, dtype: float64
grouped.apply(get_stats).unstack()
count | max | mean | min | |
---|---|---|---|---|
data1 | ||||
(-3.212, -1.547] | 61.0 | 1.681975 | -0.034289 | -2.751665 |
(-1.547, 0.111] | 493.0 | 3.286570 | -0.115413 | -3.273816 |
(0.111, 1.769] | 406.0 | 3.832312 | -0.017925 | -3.711830 |
(1.769, 3.427] | 40.0 | 2.301913 | 0.029012 | -3.133572 |
這些都是長度相等的桶。要根據樣本分位數得到大小相等的桶,使用qcut即可。傳入labels=False即可只獲取分位數的編號:
grouping = pd.qcut(frame.data1, 10, labels=False)
grouped = frame.data2.groupby(grouping)
grouped.apply(get_stats).unstack()
count | max | mean | min | |
---|---|---|---|---|
data1 | ||||
0 | 100.0 | 1.717476 | -0.017711 | -2.751665 |
1 | 100.0 | 2.217258 | -0.213915 | -2.871384 |
2 | 100.0 | 2.685711 | -0.133270 | -2.831741 |
3 | 100.0 | 3.286570 | -0.081659 | -2.920534 |
4 | 100.0 | 2.388036 | -0.145312 | -3.273816 |
5 | 100.0 | 2.903698 | 0.043201 | -3.259944 |
6 | 100.0 | 2.352234 | -0.034443 | -2.187147 |
7 | 100.0 | 2.184320 | -0.123537 | -3.711830 |
8 | 100.0 | 3.832312 | 0.134130 | -2.704397 |
9 | 100.0 | 2.301913 | -0.078561 | -3.133572 |
我們會在第12章詳細講解pandas的Categorical類型。
示例:用特定於分組的值填充缺失值
對於缺失數據的清理工作,有時你會用dropna將其替換掉,而有時則可能會希望用一個固定值或由數據集本身所衍生出來的值去填充NA值。這時就得使用fillna這個工具了。在下面這個例子中,我用平均值去填充NA值:
s = pd.Series(np.random.randn(6))
s[::2] = np.nan
s
0 NaN
1 -0.729934
2 NaN
3 1.097146
4 NaN
5 0.836751
dtype: float64
s.fillna(s.mean())
0 0.401321
1 -0.729934
2 0.401321
3 1.097146
4 0.401321
5 0.836751
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 0.498135
New York -0.231511
Vermont -0.021277
Florida 0.310654
Oregon 0.475931
Nevada 0.027816
California -1.477541
Idaho -1.618531
dtype: float64
[‘East’] * 4產生了一個列表,包括了[‘East’]中元素的四個拷貝。將這些列表串聯起來。
將一些值設為缺失:
data[['Vermont', 'Nevada', 'Idaho']] = np.nan
data
Ohio 0.498135
New York -0.231511
Vermont NaN
Florida 0.310654
Oregon 0.475931
Nevada NaN
California -1.477541
Idaho NaN
dtype: float64
data.groupby(group_key).mean()
East 0.192426
West -0.500805
dtype: float64
我們可以用分組平均值去填充NA值:
fill_mean = lambda g: g.fillna(g.mean())
data.groupby(group_key).apply(fill_mean)
Ohio 0.498135
New York -0.231511
Vermont 0.192426
Florida 0.310654
Oregon 0.475931
Nevada -0.500805
California -1.477541
Idaho -0.500805
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 0.498135
New York -0.231511
Vermont 0.500000
Florida 0.310654
Oregon 0.475931
Nevada -1.000000
California -1.477541
Idaho -1.000000
dtype: float64
示例:隨機采樣和排列
假設你想要從一個大數據集中隨機抽取(進行替換或不替換)樣本以進行蒙特卡羅模擬(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)
KD 10
KC 10
JD 10
6C 6
5H 5
dtype: int64
假設你想要從每種花色中隨機抽取兩張牌。由於花色是牌名的最后一個字符,所以我們可以據此進行分組,並使用apply:
get_suit = lambda card: card[-1] # last letter is suit
deck.groupby(get_suit).apply(draw, n=2)
C 8C 8
7C 7
D 5D 5
2D 2
H AH 1
JH 10
S QS 10
JS 10
dtype: int64
或者,也可以這樣寫:
deck.groupby(get_suit, group_keys=False).apply(draw, n=2)
JC 10
QC 10
9D 9
AD 1
2H 2
4H 4
2S 2
9S 9
dtype: int64
示例:分組加權平均數和相關系數
根據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 | -1.037496 | 0.104537 |
1 | a | 0.906086 | 0.797886 |
2 | a | -1.232700 | 0.322842 |
3 | a | -0.770139 | 0.484403 |
4 | b | -0.810745 | 0.394332 |
5 | b | -1.023347 | 0.573236 |
6 | b | -0.220570 | 0.854043 |
7 | b | 0.164519 | 0.200136 |
然后可以利用category計算分組加權平均數:
grouped = df.groupby('category')
get_wavg = lambda g: np.average(g['data'], weights=g['weights'])
grouped.apply(get_wavg)
category
a -0.091555
b -0.525176
dtype: float64
另一個例子,考慮一個來自Yahoo!Finance的數據集,其中含有幾只股票和標准普爾500指數(符號SPX)的收盤價:
close_px = pd.read_csv('examples/stock_px_2.csv', parse_dates=True,
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'])
接下來,我們使用pct_change計算close_px的百分比變化:
rets = close_px.pct_change().dropna()
最后,我們用年對百分比變化進行分組,可以用一個一行的函數,從每行的標簽返回每個datetime標簽的year屬性:
get_year = lambda x: x.year
by_year = rets.groupby(get_year)
by_year.apply(spx_corr)
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 |
當然,你還可以計算列與列之間的相關系數。這里,我們計算Apple和Microsoft的年相關系數:
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
示例:組級別的線性回歸
順着上一個例子繼續,你可以用groupby執行更為復雜的分組統計分析,只要函數返回的是pandas對象或標量值即可。例如,我可以定義下面這個regress函數(利用statsmodels計量經濟學庫)對各數據塊執行普通最小二乘法(Ordinary Least Squares,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 |
tips.groupby(['day','smoker']).mean()
total_bill | tip | size | tip_pct | ||
---|---|---|---|---|---|
day | smoker | ||||
Fri | No | 18.420000 | 2.812500 | 2.250000 | 0.151650 |
Yes | 16.813333 | 2.714000 | 2.066667 | 0.174783 | |
Sat | No | 19.661778 | 3.102889 | 2.555556 | 0.158048 |
Yes | 21.276667 | 2.875476 | 2.476190 | 0.147906 | |
Sun | No | 20.506667 | 3.167895 | 2.929825 | 0.160113 |
Yes | 24.120000 | 3.516842 | 2.578947 | 0.187250 | |
Thur | No | 17.113111 | 2.673778 | 2.488889 | 0.160298 |
Yes | 19.190588 | 3.030000 | 2.352941 | 0.163863 |
可以用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 |
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的參數說明
|參數|說明|
|values|待聚合的列的名稱,默認聚合所有數值列|
|index|用於分組的列名或其他分組鍵,出現在結果透視表的行|
|columns|用於分組的列名或其他分組鍵,出現在結果透視表的列|
|aggfunc|聚合函數或函數列表,默認是mean,可以是任何對groupby有效的函數|
|fill_value|用於替換結果中的缺失值|
|dropna|如果為True,不添加條目都為NA的列|
|margins|添加行/列小計和總計,默認為False|
交叉表:crosstab
交叉表(cross-tabulation,簡稱crosstab)是一種用於計算分組頻率的特殊透視表。看下面的例子:
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數據分組工具既有助於數據清理,也有助於建模或統計分析工作。在第14章,我們會看幾個例子,對真實數據使用groupby。
在下一章,我們將關注時間序列數據。