《利用Python進行數據分析·第2版》第10章(中) 數據聚合與分組運算

《利用Python進行數據分析·第2版》第10章(中) 數據聚合與分組運算

來自專欄 Python程序員12 人贊了文章

作者:SeanCheney Python愛好者社區專欄作者

簡書專欄:jianshu.com/u/130f76596

前文傳送門:

【翻譯】《利用Python進行數據分析·第2版》第1章 準備工作

【翻譯】《利用Python進行數據分析·第2版》第2章(上)Python語法基礎,IPython和Jupyter

【翻譯】《利用Python進行數據分析·第2版》第2章(中)Python語法基礎,IPython和Jupyter

【翻譯】《利用Python進行數據分析·第2版》第2章(下)Python語法基礎,IPython和Jupyter

【翻譯】《利用Python進行數據分析·第2版》第3章(上)Python的數據結構、函數和文件

【翻譯】《利用Python進行數據分析·第2版》第3章(中)Python的數據結構、函數和文件

【翻譯】《利用Python進行數據分析·第2版》第3章(下)Python的數據結構、函數和文件

【翻譯】《利用Python進行數據分析·第2版》第4章(上)NumPy基礎:數組和矢量計算

【翻譯】《利用Python進行數據分析·第2版》第4章(中)NumPy基礎:數組和矢量計算

【翻譯】《利用Python進行數據分析·第2版》第4章(下)NumPy基礎:數組和矢量計算

【翻譯】《利用Python進行數據分析·第2版》第5章(上)pandas入門

【翻譯】《利用Python進行數據分析·第2版》第5章(中)pandas入門

【翻譯】《利用Python進行數據分析·第2版》第5章(下)pandas入門

【翻譯】《利用Python進行數據分析·第2版》第6章(上) 數據載入、存儲與文件格式

【翻譯】《利用Python進行數據分析·第2版》第6章(中) 數據載入、存儲與文件格式

【翻譯】《利用Python進行數據分析·第2版》第6章(下) 數據載入、存儲與文件格式

【翻譯】《利用Python進行數據分析·第2版》第7章(上)數據清洗和準備

【翻譯】《利用Python進行數據分析·第2版》第7章(中) 數據清洗和準備

【翻譯】《利用Python進行數據分析·第2版》第7章(下) 數據清洗和準備

【翻譯】《利用Python進行數據分析·第2版》第8章(上) 數據規整:聚合、合併和重塑

【翻譯】《利用Python進行數據分析·第2版》第8章(中) 數據規整:聚合、合併和重塑

【翻譯】《利用Python進行數據分析·第2版》第8章(下) 數據規整:聚合、合併和重塑

【翻譯】《利用Python進行數據分析·第2版》第9章(上) 繪圖和可視化

【翻譯】《利用Python進行數據分析·第2版》第9章(中) 繪圖和可視化

【翻譯】《利用Python進行數據分析·第2版》第9章(下) 繪圖和可視化

【翻譯】《利用Python進行數據分析·第2版》第10章(上) 數據聚合與分組運算

根據索引級別分組

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

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_dfOut[49]: cty US JP tenor 1 3 5 1 30 0.560145 -1.265934 0.119827 -1.063512 0.3328831 -2.359419 -0.199543 -1.541996 -0.970736 -1.3070302 0.286350 0.377984 -0.753887 0.331286 1.3497423 0.069877 0.246674 -0.011862 1.004812 1.327195

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

In [50]: hier_df.groupby(level=cty, axis=1).count()Out[50]: cty JP US0 2 31 2 32 2 33 2 3

10.2 數據聚合

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

表10-1 經過優化的groupby方法

你可以使用自己發明的聚合運算,還可以調用分組對象上已經定義好的任何方法。例如,quantile可以計算Series或DataFrame列的樣本分位數。

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

In [51]: dfOut[51]: data1 data2 key1 key20 -0.204708 1.393406 a one1 0.478943 0.092908 a two2 -0.519439 0.281746 b one3 -0.555730 0.769023 b two4 1.965781 1.246435 a oneIn [52]: grouped = df.groupby(key1)In [53]: grouped[data1].quantile(0.9)Out[53]: key1a 1.668413b -0.523068Name: data1, dtype: float64

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

In [54]: def peak_to_peak(arr): ....: return arr.max() - arr.min()In [55]: grouped.agg(peak_to_peak)Out[55]: data1 data2key1 a 2.170488 1.300498b 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節,我將詳細說明這到底是怎麼回事。

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

面向列的多函數應用

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

In [57]: tips = pd.read_csv(examples/tips.csv)# Add tip percentage of total billIn [58]: tips[tip_pct] = tips[tip] / tips[total_bill]In [59]: tips[:6]Out[59]: total_bill tip smoker day time size tip_pct0 16.99 1.01 No Sun Dinner 2 0.0594471 10.34 1.66 No Sun Dinner 3 0.1605422 21.01 3.50 No Sun Dinner 3 0.1665873 23.68 3.31 No Sun Dinner 2 0.1397804 24.59 3.61 No Sun Dinner 4 0.1468085 25.29 4.71 No Sun Dinner 4 0.186240

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

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

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

In [61]: grouped_pct = grouped[tip_pct]In [62]: grouped_pct.agg(mean)Out[62]: day smokerFri No 0.151650 Yes 0.174783Sat No 0.158048 Yes 0.147906Sun No 0.160113 Yes 0.187250Thur No 0.160298 Yes 0.163863Name: tip_pct, dtype: float64

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

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

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

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

In [64]: grouped_pct.agg([(foo, mean), (bar, np.std)])Out[64]: foo barday smoker Fri No 0.151650 0.028123 Yes 0.174783 0.051293Sat No 0.158048 0.039767 Yes 0.147906 0.061375Sun No 0.160113 0.042347 Yes 0.187250 0.154134Thur No 0.160298 0.038774 Yes 0.163863 0.039389

對於DataFrame,你還有更多選擇,你可以定義一組應用於全部列的一組函數,或不同的列應用不同的函數。假設我們想要對tip_pct和total_bill列計算三個統計信息:

In [65]: functions = [count, mean, max]In [66]: result = grouped[tip_pct, total_bill].agg(functions)In [67]: resultOut[67]: tip_pct total_bill count mean max count mean maxday smoker Fri No 4 0.151650 0.187735 4 18.420000 22.75 Yes 15 0.174783 0.263480 15 16.813333 40.17Sat No 45 0.158048 0.291990 45 19.661778 48.33 Yes 42 0.147906 0.325733 42 21.276667 50.81Sun No 57 0.160113 0.252672 57 20.506667 48.17 Yes 19 0.187250 0.710345 19 24.120000 45.35Thur 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參數:

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

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

In [71]: grouped.agg({tip : np.max, size : sum})Out[71]: tip sizeday smoker Fri No 3.50 9 Yes 4.73 31Sat No 9.00 115 Yes 10.00 104Sun No 6.00 167 Yes 6.50 49Thur No 6.70 112 Yes 5.00 40In [72]: grouped.agg({tip_pct : [min, max, mean, std], ....: size : sum})Out[72]: tip_pct size min max mean std sumday smoker Fri No 0.120385 0.187735 0.151650 0.028123 9 Yes 0.103555 0.263480 0.174783 0.051293 31Sat No 0.056797 0.291990 0.158048 0.039767 115 Yes 0.035638 0.325733 0.147906 0.061375 104Sun No 0.059447 0.252672 0.160113 0.042347 167 Yes 0.065660 0.710345 0.187250 0.154134 49Thur 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以禁用該功能:

In [73]: tips.groupby([day, smoker], as_index=False).mean()Out[73]: day smoker total_bill tip size tip_pct0 Fri No 18.420000 2.812500 2.250000 0.1516501 Fri Yes 16.813333 2.714000 2.066667 0.1747832 Sat No 19.661778 3.102889 2.555556 0.1580483 Sat Yes 21.276667 2.875476 2.476190 0.1479064 Sun No 20.506667 3.167895 2.929825 0.1601135 Sun Yes 24.120000 3.516842 2.578947 0.1872506 Thur No 17.113111 2.673778 2.488889 0.1602987 Thur Yes 19.190588 3.030000 2.352941 0.163863

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

10.3 apply:一般性的「拆分-應用-合併」

最通用的GroupBy方法是apply,本節剩餘部分將重點講解它。如圖10-2所示,apply會將待處理的對象拆分成多個片段,然後對各片段調用傳入的函數,最後嘗試將各片段組合到一起。

圖10-2 分組聚合示例

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

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_pct109 14.31 4.00 Yes Sat Dinner 2 0.279525183 23.17 6.50 Yes Sun Dinner 4 0.280535232 11.61 3.39 No Sat Dinner 2 0.29199067 3.07 1.00 Yes Sat Dinner 1 0.325733178 9.60 4.00 Yes Sun Dinner 2 0.416667172 7.25 5.15 Yes Sun Dinner 2 0.710345

現在,如果對smoker分組並用該函數調用apply,就會得到:

In [76]: tips.groupby(smoker).apply(top)Out[76]: total_bill tip smoker day time size tip_pctsmoker 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.291990Yes 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的函數能夠接受其他參數或關鍵字,則可以將這些內容放在函數名後面一併傳入:

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

In [78]: result = tips.groupby(smoker)[tip_pct].describe()In [79]: resultOut[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 smokerNo 0.291990 Yes 0.710345 In [80]: result.unstack(smoker)Out[80]: smokercount No 151.000000 Yes 93.000000mean No 0.159328 Yes 0.163196std No 0.039910 Yes 0.085119min No 0.056797 Yes 0.03563825% No 0.136906 Yes 0.10677150% No 0.155625 Yes 0.15384675% No 0.185014 Yes 0.195059max No 0.291990 Yes 0.710345dtype: float64

在GroupBy中,當你調用諸如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_pct88 24.71 5.85 No Thur Lunch 2 0.236746185 20.69 5.00 No Sun Dinner 5 0.24166351 10.29 2.60 No Sun Dinner 2 0.252672149 7.51 2.00 No Thur Lunch 2 0.266312232 11.61 3.39 No Sat Dinner 2 0.291990109 14.31 4.00 Yes Sat Dinner 2 0.279525183 23.17 6.50 Yes Sun Dinner 4 0.28053567 3.07 1.00 Yes Sat Dinner 1 0.325733178 9.60 4.00 Yes Sun Dinner 2 0.416667172 7.25 5.15 Yes Sun Dinner 2 0.710345

推薦閱讀:

Graph圖演算法 Hits&Page Rank
python中數據基本分析過程
數據掩埋——在大數據時代里悄然遁走
大話機器學習之窮人如何玩轉數據挖掘
「機器學習」和「MHKQ因子擇時模型」相關前沿研究丨優礦金工深度報告5月篇

TAG:數據分析 | 數據挖掘 | Python |