第10章 數(shù)據(jù)聚合與分組運算

資料來源:https://github.com/BrambleXu/pydata-notebook

對數(shù)據(jù)集進行分組并對各組應用一個函數(shù)(無論是聚合還是轉換)澜建,通常是數(shù)據(jù)分析工作中的重要環(huán)節(jié)冕碟。在將數(shù)據(jù)集加載全跨、融合、準備好之后,通常就是計算分組統(tǒng)計或生成透視表。pandas提供了一個靈活高效的gruopby功能,它使你能以一種自然的方式對數(shù)據(jù)集進行切片欲间、切塊、摘要等操作断部。

關系型數(shù)據(jù)庫和SQL(Structured Query Language猎贴,結構化查詢語言)能夠如此流行的原因之一就是其能夠方便地對數(shù)據(jù)進行連接、過濾蝴光、轉換和聚合她渴。但是,像SQL這樣的查詢語言所能執(zhí)行的分組運算的種類很有限虱疏。在本章中你將會看到惹骂,由于Python和pandas強大的表達能力,我們可以執(zhí)行復雜得多的分組運算(利用任何可以接受pandas對象或NumPy數(shù)組的函數(shù))做瞪。在本章中对粪,你將會學到:

  • 使用一個或多個鍵(形式可以是函數(shù)、數(shù)組或DataFrame列名)分割pandas對象装蓬。
  • 計算分組的概述統(tǒng)計著拭,比如數(shù)量、平均值或標準差牍帚,或是用戶定義的函數(shù)儡遮。
  • 應用組內(nèi)轉換或其他運算,如規(guī)格化暗赶、線性回歸鄙币、排名或選取子集等肃叶。
  • 計算透視表或交叉表。
  • 執(zhí)行分位數(shù)分析以及其它統(tǒng)計分組分析十嘿。

筆記:對時間序列數(shù)據(jù)的聚合(groupby的特殊用法之一)也稱作重采樣(resampling)因惭,本書將在第11章中單獨對其進行講解。

10.1 GroupBy機制

Hadley Wickham(許多熱門R語言包的作者)創(chuàng)造了一個用于表示分組運算的術語"split-apply-combine"(拆分-應用-合并)绩衷。第一個階段蹦魔,pandas對象(無論是Series、DataFrame還是其他的)中的數(shù)據(jù)會根據(jù)你所提供的一個或多個鍵被拆分(split)為多組咳燕。拆分操作是在對象的特定軸上執(zhí)行的勿决。例如,DataFrame可以在其行(axis=0)或列(axis=1)上進行分組招盲。然后低缩,將一個函數(shù)應用(apply)到各個分組并產(chǎn)生一個新值。最后宪肖,所有這些函數(shù)的執(zhí)行結果會被合并(combine)到最終的結果對象中表制。結果對象的形式一般取決于數(shù)據(jù)上所執(zhí)行的操作健爬。圖10-1大致說明了一個簡單的分組聚合過程控乾。

圖10-1 分組聚合演示

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

  • 列表或數(shù)組娜遵,其長度與待分組的軸一樣蜕衡。
  • 表示DataFrame某個列名的值。
  • 字典或Series设拟,給出待分組軸上的值與分組名之間的對應關系慨仿。
  • 函數(shù),用于處理軸索引或索引中的各個標簽纳胧。

注意镰吆,后三種都只是快捷方式而已,其最終目的仍然是產(chǎn)生一組用于拆分對象的值跑慕。如果覺得這些東西看起來很抽象万皿,不用擔心,我將在本章中給出大量有關于此的示例核行。首先來看看下面這個非常簡單的表格型數(shù)據(jù)集(以DataFrame的形式):

In [10]: 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)})

In [11]: df
Out[11]: 
      data1     data2 key1 key2
0 -0.204708  1.393406    a  one
1  0.478943  0.092908    a  two
2 -0.519439  0.281746    b  one
3 -0.555730  0.769023    b  two
4  1.965781  1.246435    a  one

假設你想要按key1進行分組牢硅,并計算data1列的平均值。實現(xiàn)該功能的方式有很多芝雪,而我們這里要用的是:訪問data1减余,并根據(jù)key1調(diào)用groupby:

In [12]: grouped = df['data1'].groupby(df['key1'])

In [13]: grouped
Out[13]: <pandas.core.groupby.SeriesGroupBy object at 0x7faa31537390>

變量grouped是一個GroupBy對象。它實際上還沒有進行任何計算惩系,只是含有一些有關分組鍵df['key1']的中間數(shù)據(jù)而已位岔。換句話說如筛,該對象已經(jīng)有了接下來對各分組執(zhí)行運算所需的一切信息。例如抒抬,我們可以調(diào)用GroupBy的mean方法來計算分組平均值:

In [14]: grouped.mean()
Out[14]: 
key1
a    0.746672
b   -0.537585
Name: data1, dtype: float64

稍后我將詳細講解.mean()的調(diào)用過程妙黍。這里最重要的是,數(shù)據(jù)(Series)根據(jù)分組鍵進行了聚合瞧剖,產(chǎn)生了一個新的Series拭嫁,其索引為key1列中的唯一值。之所以結果中索引的名稱為key1抓于,是因為原始DataFrame的列df['key1']就叫這個名字做粤。

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

In [15]: means = df['data1'].groupby([df['key1'], df['key2']]).mean()

In [16]: means
Out[16]: 
key1  key2
a     one     0.880536
      two     0.478943
b     one    -0.519439
      two    -0.555730
Name: data1, dtype: float64

這里捉撮,我通過兩個鍵對數(shù)據(jù)進行了分組怕品,得到的Series具有一個層次化索引(由唯一的鍵對組成):

In [17]: means.unstack()
Out[17]: 
key2       one       two
key1                    
a     0.880536  0.478943
b    -0.519439 -0.555730

在這個例子中,分組鍵均為Series巾遭。實際上肉康,分組鍵可以是任何長度適當?shù)臄?shù)組:

In [18]: states = np.array(['Ohio', 'California', 'California', 'Ohio', 'Ohio'])

In [19]: years = np.array([2005, 2005, 2006, 2005, 2006])

In [20]: df['data1'].groupby([states, years]).mean()
Out[20]: 
California  2005    0.478943
            2006   -0.519439
Ohio        2005   -0.380219
            2006    1.965781
Name: data1, dtype: float64

通常,分組信息就位于相同的要處理DataFrame中灼舍。這里吼和,你還可以將列名(可以是字符串、數(shù)字或其他Python對象)用作分組鍵:

In [21]: df.groupby('key1').mean()
Out[21]: 
         data1     data2
key1
a     0.746672  0.910916
b    -0.537585  0.525384

In [22]: df.groupby(['key1', 'key2']).mean()
Out[22]: 
              data1     data2
key1 key2                    
a    one   0.880536  1.319920
     two   0.478943  0.092908
b    one  -0.519439  0.281746
     two  -0.555730  0.769023

你可能已經(jīng)注意到了骑素,第一個例子在執(zhí)行df.groupby('key1').mean()時炫乓,結果中沒有key2列。這是因為df['key2']不是數(shù)值數(shù)據(jù)(俗稱“麻煩列”)献丑,所以被從結果中排除了末捣。默認情況下,所有數(shù)值列都會被聚合创橄,雖然有時可能會被過濾為一個子集箩做,稍后就會碰到。

無論你準備拿groupby做什么妥畏,都有可能會用到GroupBy的size方法邦邦,它可以返回一個含有分組大小的Series:

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

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

對分組進行迭代

GroupBy對象支持迭代,可以產(chǎn)生一組二元元組(由分組名和數(shù)據(jù)塊組成)馍管」停看下面的例子:

In [24]: for name, group in df.groupby('key1'):
   ....:     print(name)
   ....:     print(group)
   ....:
a
      data1     data2 key1 key2
0 -0.204708  1.393406    a  one
1  0.478943  0.092908    a  two
4  1.965781  1.246435    a  one
b
      data1     data2 key1 key2
2 -0.519439  0.281746    b  one
3 -0.555730  0.769023    b  two

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

In [25]: for (k1, k2), group in df.groupby(['key1', 'key2']):
   ....:     print((k1, k2))
   ....:     print(group)
   ....:
('a', 'one')
      data1     data2 key1 key2
0 -0.204708  1.393406    a  one
4  1.965781  1.246435    a  one
('a', 'two')
      data1     data2 key1 key2
1  0.478943  0.092908    a  two
('b', 'one')
      data1     data2 key1 key2
2 -0.519439  0.281746    b  one
('b', 'two')
     data1     data2 key1 key2
3 -0.55573  0.769023    b  two

當然,你可以對這些數(shù)據(jù)片段做任何操作捌锭。有一個你可能會覺得有用的運算:將這些數(shù)據(jù)片段做成一個字典:

In [26]: pieces = dict(list(df.groupby('key1')))

In [27]: pieces['b']
Out[27]: 
      data1     data2 key1 key2
2 -0.519439  0.281746    b  one
3 -0.555730  0.769023    b  two

groupby默認是在axis=0上進行分組的俘陷,通過設置也可以在其他任何軸上進行分組。拿上面例子中的df來說观谦,我們可以根據(jù)dtype對列進行分組:

In [28]: df.dtypes
Out[28]: 
data1    float64
data2    float64
key1      object
key2      object
dtype: object

In [29]: grouped = df.groupby(df.dtypes, axis=1)

可以如下打印分組:

In [30]: for dtype, group in grouped:
   ....:     print(dtype)
   ....:     print(group)
   ....:
float64
      data1     data2
0 -0.204708  1.393406
1  0.478943  0.092908
2 -0.519439  0.281746
3 -0.555730  0.769023
4  1.965781  1.246435
object
  key1 key2
0    a  one
1    a  two
2    b  one
3    b  two
4    a  one

選取一列或列的子集

對于由DataFrame產(chǎn)生的GroupBy對象拉盾,如果用一個(單個字符串)或一組(字符串數(shù)組)列名對其進行索引,就能實現(xiàn)選取部分列進行聚合的目的豁状。也就是說:

df.groupby('key1')['data1']
df.groupby('key1')[['data2']]

是以下代碼的語法糖:

df['data1'].groupby(df['key1'])
df[['data2']].groupby(df['key1'])

尤其對于大數(shù)據(jù)集捉偏,很可能只需要對部分列進行聚合。例如泻红,在前面那個數(shù)據(jù)集中夭禽,如果只需計算data2列的平均值并以DataFrame形式得到結果,可以這樣寫:

In [31]: df.groupby(['key1', 'key2'])[['data2']].mean()
Out[31]: 
              data2
key1 key2          
a    one   1.319920
     two   0.092908
b    one   0.281746
     two   0.769023

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

In [32]: s_grouped = df.groupby(['key1', 'key2'])['data2']

In [33]: s_grouped
Out[33]: <pandas.core.groupby.SeriesGroupBy object at 0x7faa30c78da0>

In [34]: s_grouped.mean()
Out[34]: 
key1  key2
a     one     1.319920
      two     0.092908
b     one     0.281746
      two     0.769023
Name: data2, dtype: float64

通過字典或Series進行分組

除數(shù)組以外谊路,分組信息還可以其他形式存在讹躯。來看另一個示例DataFrame:

In [35]: people = pd.DataFrame(np.random.randn(5, 5),
   ....:                       columns=['a', 'b', 'c', 'd', 'e'],
   ....:                       index=['Joe', 'Steve', 'Wes', 'Jim', 'Travis'])

In [36]: people.iloc[2:3, [1, 2]] = np.nan # Add a few NA values

In [37]: people
Out[37]: 
               a         b         c         d         e
Joe     1.007189 -1.296221  0.274992  0.228913  1.352917
Steve   0.886429 -2.001637 -0.371843  1.669025 -0.438570
Wes    -0.539741       NaN       NaN -1.021228 -0.577087
Jim     0.124121  0.302614  0.523772  0.000940  1.343810
Travis -0.713544 -0.831154 -2.370232 -1.860761 -0.860757

現(xiàn)在,假設已知列的分組關系缠劝,并希望根據(jù)分組計算列的和:

In [38]: mapping = {'a': 'red', 'b': 'red', 'c': 'blue',
   ....:            'd': 'blue', 'e': 'red', 'f' : 'orange'}

現(xiàn)在潮梯,你可以將這個字典傳給groupby,來構造數(shù)組惨恭,但我們可以直接傳遞字典(我包含了鍵“f”來強調(diào)秉馏,存在未使用的分組鍵是可以的):

In [39]: by_column = people.groupby(mapping, axis=1)

In [40]: by_column.sum()
Out[40]: 
            blue       red
Joe     0.503905  1.063885
Steve   1.297183 -1.553778
Wes    -1.021228 -1.116829
Jim     0.524712  1.770545
Travis -4.230992 -2.405455

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

In [41]: map_series = pd.Series(mapping)

In [42]: map_series
Out[42]: 
a       red
b       red
c      blue
d      blue
e       red
f    orange
dtype: object

In [43]: people.groupby(map_series, axis=1).count()
Out[43]: 
        blue  red
Joe        2    3
Steve      2    3
Wes        1    2
Jim        2    3
Travis     2    3

通過函數(shù)進行分組

比起使用字典或Series喉恋,使用Python函數(shù)是一種更原生的方法定義分組映射沃饶。任何被當做分組鍵的函數(shù)都會在各個索引值上被調(diào)用一次母廷,其返回值就會被用作分組名稱轻黑。具體點說,以上一小節(jié)的示例DataFrame為例琴昆,其索引值為人的名字氓鄙。你可以計算一個字符串長度的數(shù)組,更簡單的方法是傳入len函數(shù):

In [44]: people.groupby(len).sum()
Out[44]: 
          a         b         c         d         e
3  0.591569 -0.993608  0.798764 -0.791374  2.119639
5  0.886429 -2.001637 -0.371843  1.669025 -0.438570
6 -0.713544 -0.831154 -2.370232 -1.860761 -0.860757

將函數(shù)跟數(shù)組业舍、列表抖拦、字典、Series混合使用也不是問題舷暮,因為任何東西在內(nèi)部都會被轉換為數(shù)組:

In [45]: key_list = ['one', 'one', 'one', 'two', 'two']

In [46]: people.groupby([len, key_list]).min()
Out[46]: 
              a         b         c         d         e
3 one -0.539741 -1.296221  0.274992 -1.021228 -0.577087
  two  0.124121  0.302614  0.523772  0.000940  1.343810
5 one  0.886429 -2.001637 -0.371843  1.669025 -0.438570
6 two -0.713544 -0.831154 -2.370232 -1.860761 -0.860757

根據(jù)索引級別分組

層次化索引數(shù)據(jù)集最方便的地方就在于它能夠根據(jù)軸索引的一個級別進行聚合:

In [47]: columns = pd.MultiIndex.from_arrays([['US', 'US', 'US', 'JP', 'JP'],
   ....:                                     [1, 3, 5, 1, 3]],
   ....:                                     names=['cty', 'tenor'])

In [48]: hier_df = pd.DataFrame(np.random.randn(4, 5), columns=columns)

In [49]: hier_df
Out[49]: 
cty          US                            JP          
tenor         1         3         5         1         3
0      0.560145 -1.265934  0.119827 -1.063512  0.332883
1     -2.359419 -0.199543 -1.541996 -0.970736 -1.307030
2      0.286350  0.377984 -0.753887  0.331286  1.349742
3      0.069877  0.246674 -0.011862  1.004812  1.327195

要根據(jù)級別分組态罪,使用level關鍵字傳遞級別序號或名字:

In [50]: hier_df.groupby(level='cty', axis=1).count()
Out[50]: 
cty  JP  US
0     2   3
1     2   3
2     2   3
3     2   3

10.2 數(shù)據(jù)聚合

聚合指的是任何能夠從數(shù)組產(chǎn)生標量值的數(shù)據(jù)轉換過程。之前的例子已經(jīng)用過一些下面,比如mean复颈、count、min以及sum等沥割。你可能想知道在GroupBy對象上調(diào)用mean()時究竟發(fā)生了什么耗啦。許多常見的聚合運算(如表10-1所示)都有進行優(yōu)化凿菩。然而,除了這些方法帜讲,你還可以使用其它的衅谷。

表10-1 經(jīng)過優(yōu)化的groupby方法

你可以使用自己發(fā)明的聚合運算,還可以調(diào)用分組對象上已經(jīng)定義好的任何方法似将。例如获黔,quantile可以計算Series或DataFrame列的樣本分位數(shù)。

雖然quantile并沒有明確地實現(xiàn)于GroupBy在验,但它是一個Series方法肢执,所以這里是能用的。實際上译红,GroupBy會高效地對Series進行切片预茄,然后對各片調(diào)用piece.quantile(0.9),最后將這些結果組裝成最終結果:

In [51]: df
Out[51]: 
      data1     data2 key1 key2
0 -0.204708  1.393406    a  one
1  0.478943  0.092908    a  two
2 -0.519439  0.281746    b  one
3 -0.555730  0.769023    b  two
4  1.965781  1.246435    a  one

In [52]: grouped = df.groupby('key1')

In [53]: grouped['data1'].quantile(0.9)
Out[53]: 
key1
a    1.668413
b   -0.523068
Name: data1, dtype: float64

如果要使用你自己的聚合函數(shù)侦厚,只需將其傳入aggregate或agg方法即可:

In [54]: def peak_to_peak(arr):
   ....:     return arr.max() - arr.min()
In [55]: grouped.agg(peak_to_peak)
Out[55]: 
         data1     data2
key1                    
a     2.170488  1.300498
b     0.036292  0.487276

你可能注意到注意耻陕,有些方法(如describe)也是可以用在這里的,即使嚴格來講刨沦,它們并非聚合運算:

In [56]: grouped.describe()
Out[56]: 
     data1                                                              \
     count      mean       std       min       25%       50%       75%   
key1                                                                     
a      3.0  0.746672  1.109736 -0.204708  0.137118  0.478943  1.222362   
b      2.0 -0.537585  0.025662 -0.555730 -0.546657 -0.537585 -0.528512   
               data2                                                    \
max count      mean       std       min       25%       50%   
key1                                                                     
a     1.965781   3.0  0.910916  0.712217  0.092908  0.669671  1.246435   
b    -0.519439   2.0  0.525384  0.344556  0.281746  0.403565  0.525384   
                          
           75%       max  
key1                      
a     1.319920  1.393406  
b     0.647203  0.769023

在后面的10.3節(jié)诗宣,我將詳細說明這到底是怎么回事。

筆記:自定義聚合函數(shù)要比表10-1中那些經(jīng)過優(yōu)化的函數(shù)慢得多想诅。這是因為在構造中間分組數(shù)據(jù)塊時存在非常大的開銷(函數(shù)調(diào)用召庞、數(shù)據(jù)重排等)。

面向列的多函數(shù)應用

回到前面小費的例子来破。使用read_csv導入數(shù)據(jù)之后篮灼,我們添加了一個小費百分比的列tip_pct:

In [57]: tips = pd.read_csv('examples/tips.csv')

# Add tip percentage of total bill
In [58]: tips['tip_pct'] = tips['tip'] / tips['total_bill']

In [59]: tips[:6]
Out[59]: 
   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

你已經(jīng)看到,對Series或DataFrame列的聚合運算其實就是使用aggregate(使用自定義函數(shù))或調(diào)用諸如mean徘禁、std之類的方法诅诱。然而,你可能希望對不同的列使用不同的聚合函數(shù)送朱,或一次應用多個函數(shù)娘荡。其實這也好辦,我將通過一些示例來進行講解驶沼。首先炮沐,我根據(jù)天和smoker對tips進行分組:

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

注意,對于表10-1中的那些描述統(tǒng)計回怜,可以將函數(shù)名以字符串的形式傳入:

In [61]: grouped_pct = grouped['tip_pct']

In [62]: grouped_pct.agg('mean')
Out[62]: 
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

如果傳入一組函數(shù)或函數(shù)名大年,得到的DataFrame的列就會以相應的函數(shù)命名:

In [63]: grouped_pct.agg(['mean', 'std', peak_to_peak])
Out[63]: 
                 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

這里,我們傳遞了一組聚合函數(shù)進行聚合,獨立對數(shù)據(jù)分組進行評估鲜戒。

你并非一定要接受GroupBy自動給出的那些列名专控,特別是lambda函數(shù),它們的名稱是'<lambda>'遏餐,這樣的辨識度就很低了(通過函數(shù)的name屬性看看就知道了)伦腐。因此,如果傳入的是一個由(name,function)元組組成的列表失都,則各元組的第一個元素就會被用作DataFrame的列名(可以將這種二元元組列表看做一個有序映射):

In [64]: grouped_pct.agg([('foo', 'mean'), ('bar', np.std)])
Out[64]: 
                  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柏蘑,你還有更多選擇,你可以定義一組應用于全部列的一組函數(shù)粹庞,或不同的列應用不同的函數(shù)咳焚。假設我們想要對tip_pct和total_bill列計算三個統(tǒng)計信息:

In [65]: functions = ['count', 'mean', 'max']

In [66]: result = grouped['tip_pct', 'total_bill'].agg(functions)

In [67]: result
Out[67]: 
            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參數(shù):

In [68]: result['tip_pct']
Out[68]: 
             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

跟前面一樣流码,這里也可以傳入帶有自定義名稱的一組元組:

In [69]: ftuples = [('Durchschnitt', 'mean'),('Abweichung', np.var)]

In [70]: grouped['tip_pct', 'total_bill'].agg(ftuples)
Out[70]: 
                 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

現(xiàn)在又官,假設你想要對一個列或不同的列應用不同的函數(shù)。具體的辦法是向agg傳入一個從列名映射到函數(shù)的字典:

In [71]: grouped.agg({'tip' : np.max, 'size' : 'sum'})
Out[71]: 
               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

In [72]: grouped.agg({'tip_pct' : ['min', 'max', 'mean', 'std'],
   ....:              'size' : 'sum'})
Out[72]: 
              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

只有將多個函數(shù)應用到至少一列時漫试,DataFrame才會擁有層次化的列六敬。

以“沒有行索引”的形式返回聚合數(shù)據(jù)

到目前為止,所有示例中的聚合數(shù)據(jù)都有由唯一的分組鍵組成的索引(可能還是層次化的)驾荣。由于并不總是需要如此外构,所以你可以向groupby傳入as_index=False以禁用該功能:

In [73]: tips.groupby(['day', 'smoker'], as_index=False).mean()
Out[73]: 
    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

當然,對結果調(diào)用reset_index也能得到這種形式的結果播掷。使用as_index=False方法可以避免一些不必要的計算审编。

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

最通用的GroupBy方法是apply,本節(jié)剩余部分將重點講解它叮趴。如圖10-2所示割笙,apply會將待處理的對象拆分成多個片段,然后對各片段調(diào)用傳入的函數(shù)眯亦,最后嘗試將各片段組合到一起。

圖10-2 分組聚合示例

回到之前那個小費數(shù)據(jù)集般码,假設你想要根據(jù)分組選出最高的5個tip_pct值妻率。首先,編寫一個選取指定列具有最大值的行的函數(shù):

In [74]: def top(df, n=5, column='tip_pct'):
   ....:     return df.sort_values(by=column)[-n:]

In [75]: top(tips, n=6)
Out[75]: 
     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

現(xiàn)在板祝,如果對smoker分組并用該函數(shù)調(diào)用apply宫静,就會得到:

In [76]: tips.groupby('smoker').apply(top)
Out[76]: 
            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

這里發(fā)生了什么?top函數(shù)在DataFrame的各個片段上調(diào)用,然后結果由pandas.concat組裝到一起孤里,并以分組名稱進行了標記伏伯。于是,最終結果就有了一個層次化索引捌袜,其內(nèi)層索引值來自原DataFrame说搅。

如果傳給apply的函數(shù)能夠接受其他參數(shù)或關鍵字,則可以將這些內(nèi)容放在函數(shù)名后面一并傳入:

In [77]: tips.groupby(['smoker', 'day']).apply(top, n=1, column='total_bill')
Out[77]: 
                 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

筆記:除這些基本用法之外虏等,能否充分發(fā)揮apply的威力很大程度上取決于你的創(chuàng)造力弄唧。傳入的那個函數(shù)能做什么全由你說了算,它只需返回一個pandas對象或標量值即可霍衫。本章后續(xù)部分的示例主要用于講解如何利用groupby解決各種各樣的問題候引。

可能你已經(jīng)想起來了,之前我在GroupBy對象上調(diào)用過describe:

In [78]: result = tips.groupby('smoker')['tip_pct'].describe()

In [79]: result
Out[79]: 
        count      mean       std       min       25%       50%       75%  \
smoker                                                                      
No      151.0  0.159328  0.039910  0.056797  0.136906  0.155625  0.185014   
Yes      93.0  0.163196  0.085119  0.035638  0.106771  0.153846  0.195059   
             max  
smoker

No      0.291990  
Yes     0.710345  

In [80]: result.unstack('smoker')
Out[80]: 
       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中敦跌,當你調(diào)用諸如describe之類的方法時澄干,實際上只是應用了下面兩條代碼的快捷方式而已:

f = lambda x: x.describe()
grouped.apply(f)

禁止分組鍵

從上面的例子中可以看出,分組鍵會跟原始對象的索引共同構成結果對象中的層次化索引柠傍。將group_keys=False傳入groupby即可禁止該效果:

In [81]: tips.groupby('smoker', group_keys=False).apply(top)
Out[81]: 
     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

分位數(shù)和桶分析

我曾在第8章中講過傻寂,pandas有一些能根據(jù)指定面元或樣本分位數(shù)將數(shù)據(jù)拆分成多塊的工具(比如cut和qcut)。將這些函數(shù)跟groupby結合起來携兵,就能非常輕松地實現(xiàn)對數(shù)據(jù)集的桶(bucket)或分位數(shù)(quantile)分析了疾掰。以下面這個簡單的隨機數(shù)據(jù)集為例,我們利用cut將其裝入長度相等的桶中:

In [82]: frame = pd.DataFrame({'data1': np.random.randn(1000),
   ....:                       'data2': np.random.randn(1000)})

In [83]: quartiles = pd.cut(frame.data1, 4)

In [84]: quartiles[:10]
Out[84]: 
0     (-1.23, 0.489]
1    (-2.956, -1.23]
2     (-1.23, 0.489]
3     (0.489, 2.208]
4     (-1.23, 0.489]
5     (0.489, 2.208]
6     (-1.23, 0.489]
7     (-1.23, 0.489]
8     (0.489, 2.208]
9     (0.489, 2.208]
Name: data1, dtype: category
Categories (4, interval[float64]): [(-2.956, -1.23] < (-1.23, 0.489] < (0.489, 2.
208] < (2.208, 3.928]]

由cut返回的Categorical對象可直接傳遞到groupby徐紧。因此静檬,我們可以像下面這樣對data2列做一些統(tǒng)計計算:

In [85]: def get_stats(group):
   ....:     return {'min': group.min(), 'max': group.max(),
   ....:             'count': group.count(), 'mean': group.mean()}

In [86]: grouped = frame.data2.groupby(quartiles)

In [87]: grouped.apply(get_stats).unstack()
Out[87]: 
                 count       max      mean       min
data1                                               
(-2.956, -1.23]   95.0  1.670835 -0.039521 -3.399312
(-1.23, 0.489]   598.0  3.260383 -0.002051 -2.989741
(0.489, 2.208]   297.0  2.954439  0.081822 -3.745356
(2.208, 3.928]    10.0  1.765640  0.024750 -1.929776

這些都是長度相等的桶。要根據(jù)樣本分位數(shù)得到大小相等的桶并级,使用qcut即可拂檩。傳入labels=False即可只獲取分位數(shù)的編號:

# Return quantile numbers
In [88]: grouping = pd.qcut(frame.data1, 10, labels=False)

In [89]: grouped = frame.data2.groupby(grouping)

In [90]: grouped.apply(get_stats).unstack()
Out[90]: 
       count       max      mean       min
data1                                     
0      100.0  1.670835 -0.049902 -3.399312
1      100.0  2.628441  0.030989 -1.950098
2      100.0  2.527939 -0.067179 -2.925113
3      100.0  3.260383  0.065713 -2.315555
4      100.0  2.074345 -0.111653 -2.047939
5      100.0  2.184810  0.052130 -2.989741
6      100.0  2.458842 -0.021489 -2.223506
7      100.0  2.954439 -0.026459 -3.056990
8      100.0  2.735527  0.103406 -3.745356
9      100.0  2.377020  0.220122 -2.064111

我們會在第12章詳細講解pandas的Categorical類型。

示例:用特定于分組的值填充缺失值

對于缺失數(shù)據(jù)的清理工作嘲碧,有時你會用dropna將其替換掉稻励,而有時則可能會希望用一個固定值或由數(shù)據(jù)集本身所衍生出來的值去填充NA值。這時就得使用fillna這個工具了愈涩。在下面這個例子中望抽,我用平均值去填充NA值:

In [91]: s = pd.Series(np.random.randn(6))

In [92]: s[::2] = np.nan

In [93]: s
Out[93]: 
0         NaN
1   -0.125921
2         NaN
3   -0.884475
4         NaN
5    0.227290
dtype: float64

In [94]: s.fillna(s.mean())
Out[94]: 
0   -0.261035
1   -0.125921
2   -0.261035
3   -0.884475
4   -0.261035
5    0.227290
dtype: float64

假設你需要對不同的分組填充不同的值。一種方法是將數(shù)據(jù)分組履婉,并使用apply和一個能夠對各數(shù)據(jù)塊調(diào)用fillna的函數(shù)即可煤篙。下面是一些有關美國幾個州的示例數(shù)據(jù),這些州又被分為東部和西部:

In [95]: states = ['Ohio', 'New York', 'Vermont', 'Florida',
   ....:           'Oregon', 'Nevada', 'California', 'Idaho']

In [96]: group_key = ['East'] * 4 + ['West'] * 4

In [97]: data = pd.Series(np.random.randn(8), index=states)

In [98]: data
Out[98]: 
Ohio          0.922264
New York     -2.153545
Vermont      -0.365757
Florida      -0.375842
Oregon        0.329939
Nevada        0.981994
California    1.105913
Idaho        -1.613716
dtype: float64

['East'] * 4產(chǎn)生了一個列表毁腿,包括了['East']中元素的四個拷貝辑奈。將這些列表串聯(lián)起來苛茂。

將一些值設為缺失:

In [99]: data[['Vermont', 'Nevada', 'Idaho']] = np.nan

In [100]: data
Out[100]: 
Ohio          0.922264
New York     -2.153545
Vermont            NaN
Florida      -0.375842
Oregon        0.329939
Nevada             NaN
California    1.105913
Idaho              NaN
dtype: float64

In [101]: data.groupby(group_key).mean()
Out[101]: 
East   -0.535707
West    0.717926
dtype: float64

我們可以用分組平均值去填充NA值:

In [102]: fill_mean = lambda g: g.fillna(g.mean())

In [103]: data.groupby(group_key).apply(fill_mean)
Out[103]: 
Ohio          0.922264
New York     -2.153545
Vermont      -0.535707
Florida      -0.375842
Oregon        0.329939
Nevada        0.717926
California    1.105913
Idaho         0.717926
dtype: float64

另外,也可以在代碼中預定義各組的填充值鸠窗。由于分組具有一個name屬性妓羊,所以我們可以拿來用一下:

In [104]: fill_values = {'East': 0.5, 'West': -1}

In [105]: fill_func = lambda g: g.fillna(fill_values[g.name])

In [106]: data.groupby(group_key).apply(fill_func)
Out[106]: 
Ohio          0.922264
New York     -2.153545
Vermont       0.500000
Florida      -0.375842
Oregon        0.329939
Nevada       -1.000000
California    1.105913
Idaho        -1.000000
dtype: float64

示例:隨機采樣和排列

假設你想要從一個大數(shù)據(jù)集中隨機抽取(進行替換或不替換)樣本以進行蒙特卡羅模擬(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)

現(xiàn)在我有了一個長度為52的Series丙猬,其索引包括牌名涨颜,值則是21點或其他游戲中用于計分的點數(shù)(為了簡單起見,我當A的點數(shù)為1):

In [108]: deck[:13]
Out[108]: 
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

現(xiàn)在茧球,根據(jù)我上面所講的庭瑰,從整副牌中抽出5張,代碼如下:

In [109]: def draw(deck, n=5):
   .....:     return deck.sample(n)

In [110]: draw(deck)
Out[110]: 
AD     1
8C     8
5H     5
KC    10
2C     2
dtype: int64

假設你想要從每種花色中隨機抽取兩張牌抢埋。由于花色是牌名的最后一個字符弹灭,所以我們可以據(jù)此進行分組,并使用apply:

In [111]: get_suit = lambda card: card[-1] # last letter is suit

In [112]: deck.groupby(get_suit).apply(draw, n=2)
Out[112]: 
C  2C     2
   3C     3
D  KD    10
   8D     8
H  KH    10
   3H     3
S  2S     2
   4S     4
dtype: int64

或者揪垄,也可以這樣寫:

In [113]: deck.groupby(get_suit, group_keys=False).apply(draw, n=2)
Out[113]: 
KC    10
JC    10
AD     1
5D     5
5H     5
6H     6
7S     7
KS    10
dtype: int64

示例:分組加權平均數(shù)和相關系數(shù)

根據(jù)groupby的“拆分-應用-合并”范式穷吮,可以進行DataFrame的列與列之間或兩個Series之間的運算(比如分組加權平均)。以下面這個數(shù)據(jù)集為例饥努,它含有分組鍵捡鱼、值以及一些權重值:

In [114]: df = pd.DataFrame({'category': ['a', 'a', 'a', 'a',
   .....:                                 'b', 'b', 'b', 'b'],
   .....:                    'data': np.random.randn(8),
   .....:                    'weights': np.random.rand(8)})

In [115]: df
Out[115]: 
  category      data   weights
0        a  1.561587  0.957515
1        a  1.219984  0.347267
2        a -0.482239  0.581362
3        a  0.315667  0.217091
4        b -0.047852  0.894406
5        b -0.454145  0.918564
6        b -0.556774  0.277825
7        b  0.253321  0.955905

然后可以利用category計算分組加權平均數(shù):

In [116]: grouped = df.groupby('category')

In [117]: get_wavg = lambda g: np.average(g['data'], weights=g['weights'])

In [118]: grouped.apply(get_wavg)
Out[118]:
category
a    0.811643
b   -0.122262
dtype: float64

另一個例子,考慮一個來自Yahoo!Finance的數(shù)據(jù)集酷愧,其中含有幾只股票和標準普爾500指數(shù)(符號SPX)的收盤價:

In [119]: close_px = pd.read_csv('examples/stock_px_2.csv', parse_dates=True,
   .....:                        index_col=0)

In [120]: 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

In [121]: close_px[-4:]
Out[121]: 
              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

來做一個比較有趣的任務:計算一個由日收益率(通過百分數(shù)變化計算)與SPX之間的年度相關系數(shù)組成的DataFrame驾诈。下面是一個實現(xiàn)辦法,我們先創(chuàng)建一個函數(shù)溶浴,用它計算每列和SPX列的成對相關系數(shù):

In [122]: spx_corr = lambda x: x.corrwith(x['SPX'])

接下來乍迄,我們使用pct_change計算close_px的百分比變化:

In [123]: rets = close_px.pct_change().dropna()

最后,我們用年對百分比變化進行分組士败,可以用一個一行的函數(shù)闯两,從每行的標簽返回每個datetime標簽的year屬性:

In [124]: get_year = lambda x: x.year

In [125]: by_year = rets.groupby(get_year)

In [126]: by_year.apply(spx_corr)
Out[126]: 
          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

當然,你還可以計算列與列之間的相關系數(shù)谅将。這里漾狼,我們計算Apple和Microsoft的年相關系數(shù):

In [127]: by_year.apply(lambda g: g['AAPL'].corr(g['MSFT']))
Out[127]: 
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

示例:組級別的線性回歸

順著上一個例子繼續(xù),你可以用groupby執(zhí)行更為復雜的分組統(tǒng)計分析戏自,只要函數(shù)返回的是pandas對象或標量值即可邦投。例如,我可以定義下面這個regress函數(shù)(利用statsmodels計量經(jīng)濟學庫)對各數(shù)據(jù)塊執(zhí)行普通最小二乘法(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

現(xiàn)在,為了按年計算AAPL對SPX收益率的線性回歸,執(zhí)行:

In [129]: by_year.apply(regress, 'AAPL', ['SPX'])
Out[129]: 
           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

10.4 透視表和交叉表

透視表(pivot table)是各種電子表格程序和其他數(shù)據(jù)分析軟件中一種常見的數(shù)據(jù)匯總工具吆视。它根據(jù)一個或多個鍵對數(shù)據(jù)進行聚合棚贾,并根據(jù)行和列上的分組鍵將數(shù)據(jù)分配到各個矩形區(qū)域中。在Python和pandas中弯淘,可以通過本章所介紹的groupby功能以及(能夠利用層次化索引的)重塑運算制作透視表绿店。DataFrame有一個pivot_table方法,此外還有一個頂級的pandas.pivot_table函數(shù)庐橙。除能為groupby提供便利之外假勿,pivot_table還可以添加分項小計,也叫做margins态鳖。

回到小費數(shù)據(jù)集转培,假設我想要根據(jù)day和smoker計算分組平均數(shù)(pivot_table的默認聚合類型),并將day和smoker放到行上:

In [130]: tips.pivot_table(index=['day', 'smoker'])
Out[130]: 
                 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

可以用groupby直接來做〗撸現(xiàn)在浸须,假設我們只想聚合tip_pct和size,而且想根據(jù)time進行分組邦泄。我將smoker放到列上删窒,把day放到行上:

In [131]: tips.pivot_table(['tip_pct', 'size'], index=['time', 'day'],
   .....:                  columns='smoker')
Out[131]: 
                 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

還可以對這個表作進一步的處理,傳入margins=True添加分項小計顺囊。這將會添加標簽為All的行和列肌索,其值對應于單個等級中所有數(shù)據(jù)的分組統(tǒng)計:

In [132]: tips.pivot_table(['tip_pct', 'size'], index=['time', 'day'],
   .....:                  columns='smoker', margins=True)
Out[132]: 
                 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值為平均數(shù):不單獨考慮煙民與非煙民(All列)特碳,不單獨考慮行分組兩個級別中的任何單項(All行)诚亚。

要使用其他的聚合函數(shù),將其傳給aggfunc即可测萎。例如亡电,使用count或len可以得到有關分組大小的交叉表(計數(shù)或頻率):

In [133]: tips.pivot_table('tip_pct', index=['time', 'smoker'], columns='day',
   .....:                  aggfunc=len, margins=True)
Out[133]: 
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:

In [134]: tips.pivot_table('tip_pct', index=['time', 'size', 'smoker'],
   .....:                  columns='day', aggfunc='mean', fill_value=0)
Out[134]: 
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
...                      ...       ...       ...       ...
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
[21 rows x 4 columns]

pivot_table的參數(shù)說明請參見表10-2硅瞧。

表10-2 pivot_table的選項

交叉表:crosstab

交叉表(cross-tabulation份乒,簡稱crosstab)是一種用于計算分組頻率的特殊透視表⊥筮螅看下面的例子:

In [138]: data
Out[138]:
   Sample Nationality    Handedness
0       1         USA  Right-handed
1       2       Japan   Left-handed
2       3         USA  Right-handed
3       4       Japan  Right-handed
4       5       Japan   Left-handed
5       6       Japan  Right-handed
6       7         USA  Right-handed
7       8         USA   Left-handed
8       9       Japan  Right-handed
9      10         USA  Right-handed

作為調(diào)查分析的一部分或辖,我們可能想要根據(jù)國籍和用手習慣對這段數(shù)據(jù)進行統(tǒng)計匯總。雖然可以用pivot_table實現(xiàn)該功能枣接,但是pandas.crosstab函數(shù)會更方便:

In [139]: pd.crosstab(data.Nationality, data.Handedness, margins=True)
Out[139]: 
Handedness   Left-handed  Right-handed  All
Nationality
Japan                  2             3    5
USA                    1             4    5
All                    3             7   10

crosstab的前兩個參數(shù)可以是數(shù)組或Series颂暇,或是數(shù)組列表。就像小費數(shù)據(jù):

In [140]: pd.crosstab([tips.time, tips.day], tips.smoker, margins=True)
Out[140]: 
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

10.5 總結

掌握pandas數(shù)據(jù)分組工具既有助于數(shù)據(jù)清理但惶,也有助于建亩欤或統(tǒng)計分析工作湿蛔。在第14章,我們會看幾個例子县爬,對真實數(shù)據(jù)使用groupby阳啥。

在下一章,我們將關注時間序列數(shù)據(jù)财喳。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末察迟,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子耳高,更是在濱河造成了極大的恐慌扎瓶,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泌枪,死亡現(xiàn)場離奇詭異概荷,居然都是意外死亡,警方通過查閱死者的電腦和手機工闺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門乍赫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人陆蟆,你說我怎么就攤上這事雷厂。” “怎么了叠殷?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵改鲫,是天一觀的道長。 經(jīng)常有香客問我林束,道長像棘,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任壶冒,我火速辦了婚禮缕题,結果婚禮上,老公的妹妹穿的比我還像新娘胖腾。我一直安慰自己烟零,他們只是感情好,可當我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布咸作。 她就那樣靜靜地躺著锨阿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪记罚。 梳的紋絲不亂的頭發(fā)上墅诡,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天,我揣著相機與錄音桐智,去河邊找鬼末早。 笑死烟馅,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的荐吉。 我是一名探鬼主播焙糟,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼口渔,長吁一口氣:“原來是場噩夢啊……” “哼样屠!你這毒婦竟也來了?” 一聲冷哼從身側響起缺脉,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤痪欲,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后攻礼,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體业踢,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年礁扮,在試婚紗的時候發(fā)現(xiàn)自己被綠了知举。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡太伊,死狀恐怖雇锡,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情僚焦,我是刑警寧澤锰提,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站芳悲,受9級特大地震影響立肘,放射性物質發(fā)生泄漏。R本人自食惡果不足惜名扛,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一谅年、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧肮韧,春花似錦融蹂、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至桩蓉,卻和暖如春淋纲,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背院究。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工洽瞬, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留本涕,地道東北人。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓伙窃,卻偏偏與公主長得像菩颖,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子为障,可洞房花燭夜當晚...
    茶點故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內(nèi)容