3.10 數據透視表

3.10 數據透視表

我們已經介紹過 GroupBy 抽象類是如何探索數據集內部的關聯性的了。數據透視表(pivot table)是一種類似的操作方法,常見於 Excel 與類似的表格應用中。數據透視表將每一列數據作爲輸入,輸出將數據不斷細分成多個維度累計信息的二維數據表。人們有時容易弄混數據透視表與 GroupBy,但我覺得數據透視表更像是一種多維的 GroupBy 累計操作。也就是說,雖然你也可以分割 - 應用 - 組合,但是分割與組合不是發生在一維索引上,而是在二維網格上(行列同時分組)。

3.10.1 演示數據透視表

這一節的示例將採用泰坦尼克號的乘客信息數據庫來演示,可以在 Seaborn 程序庫(詳情請參見 4.16 節)獲取:

import numpy as np
import pandas as pd
import seaborn as sns
titanic = sns.load_dataset('titanic')
titanic.head()
  survived pclass sex age sibsp parch fare embarked class who adult_male deck embark_town alive alone
0 0 3 male 22.0 1 0 7.2500 S Third man True NaN Southampton no False
1 1 1 female 38.0 1 0 71.2833 C First woman False C Cherbourg yes False
2 1 3 female 26.0 0 0 7.9250 S Third woman False NaN Southampton yes True
3 1 1 female 35.0 1 0 53.1000 S First woman False C Southampton yes False
4 0 3 male 35.0 0 0 8.0500 S Third man True NaN Southampton no True

這份數據包含了慘遭厄運的每位乘客的大量信息,包括性別(gender)、年齡(age)、船艙等級(class)和船票價格(fare paid)等。

3.10.2 手工製作數據透視表

在研究這些數據之前,先將它們按照性別、最終生還狀態或其他組合屬性進行分組。如果你看過前面的章節,你可能會用 GroupBy 來實現,例如這樣統計不同性別乘客的生還率:

titanic.groupby('sex')[['survived']].mean()
  survived
sex  
female 0.742038
male 0.188908

這組數據會立刻給我們一個直觀感受:總體來說,有四分之三的女性被救,但只有五分之一的男性被救!

這組數據很有用,但是我們可能還想進一步探索,同時觀察不同性別與船艙等級的生還情況。根據 GroupBy 的操作流程,我們也許能夠實現想要的結果:將船艙等級('class')與性別('sex')分組,然後選擇生還狀態('survived')列,應用均值('mean')累計函數,再將各組結果組合,最後通過行索引轉列索引操作將最裏層的行索引轉換成列索引,形成二維數組。代碼如下所示:

titanic.groupby(['sex', 'class'])['survived'].aggregate('mean').unstack()
class First Second Third
sex      
female 0.968085 0.921053 0.500000
male 0.368852 0.157407 0.135447

雖然這樣就可以更清晰地觀察乘客性別、船艙等級對其是否生還的影響,但是代碼看上去有點複雜。儘管這個管道命令的每一步都是前面介紹過的,但是要理解這個長長的語句可不是那麼容易的事。由於二維的 GroupBy 應用場景非常普遍,因此 Pandas 提供了一個快捷方式 pivot_table 來快速解決多維的累計分析任務。

3.10.3 數據透視表語法

用 DataFrame 的 pivot_table 實現的效果等同於上一節的管道命令的代碼:

titanic.pivot_table('survived', index='sex', columns='class')
class First Second Third
sex      
female 0.968085 0.921053 0.500000
male 0.368852 0.157407 0.135447

與 GroupBy 方法相比,這行代碼可讀性更強,而且取得的結果也一樣。可能與你對 20 世紀初的那場災難的猜想一致,生還率最高的是船艙等級高的女性。一等艙的女性乘客基本全部生還(露絲自然得救),而三等艙男性乘客的生還率僅爲十分之一(傑克爲愛犧牲)。

多級數據透視表

與 GroupBy 類似,數據透視表中的分組也可以通過各種參數指定多個等級。例如,我們可能想把年齡('age')也加進去作爲第三個維度,這就可以通過 pd.cut 函數將年齡進行分段:

age = pd.cut(titanic['age'], [0, 18, 80])
titanic.pivot_table('survived', ['sex', age], 'class')
  class First Second Third
sex age      
female (0, 18] 0.909091 1.000000 0.511628
(18, 80] 0.972973 0.900000 0.423729
male (0, 18] 0.800000 0.600000 0.215686
(18, 80] 0.375000 0.071429 0.133663

對某一列也可以使用同樣的策略——讓我們用 pd.qcut 將船票價格按照計數項等分爲兩份,加入數據透視表看看:

fare = pd.qcut(titanic['fare'], 2)
titanic.pivot_table('survived', ['sex', age], [fare, 'class'])
  fare (-0.001, 14.454] (14.454, 512.329]
  class First Second Third First Second Third
sex age            
female (0, 18] NaN 1.000000 0.714286 0.909091 1.000000 0.318182
(18, 80] NaN 0.880000 0.444444 0.972973 0.914286 0.391304
male (0, 18] NaN 0.000000 0.260870 0.800000 0.818182 0.178571
(18, 80] 0.0 0.098039 0.125000 0.391304 0.030303 0.192308

結果是一個帶層級索引(詳情請參見 3.6 節)的四維累計數據表,通過網格顯示不同數值之間的相關性。

其他數據透視表選項

DataFrame 的 pivot_table 方法的完整簽名如下所示:

#  Pandas 0.18版的函數簽名
DataFrame.pivot_table(data, values=None, index=None, columns=None,
                      aggfunc='mean', fill_value=None, margins=False,
                      dropna=True, margins_name='All')

我們已經介紹過前面三個參數了,現在來看看其他參數。

fill_value 和 dropna 這兩個參數用於處理缺失值,用法很簡單,我們將在後面的示例中演示其用法。

aggfunc 參數用於設置累計函數類型,默認值是均值(mean)。與 GroupBy 的用法一樣,累計函數可以用一些常見的字符串('sum'、'mean'、'count'、'min'、'max' 等)表示,也可以用標準的累計函數(np.sum()、min()、sum() 等)表示。另外,還可以通過字典爲不同的列指定不同的累計函數:

titanic.pivot_table(index='sex', columns='class',
                    aggfunc={'survived':sum, 'fare':'mean'})
  fare survived
class First Second Third First Second Third
sex            
female 106.125798 21.970121 16.118810 91 70 72
male 67.226127 19.741782 12.661633 45 17 47

需要注意的是,這裏忽略了一個參數 values。當我們爲 aggfunc 指定映射關係的時候,待透視的數值就已經確定了。 當需要計算每一組的總數時,可以通過 margins 參數來設置:

titanic.pivot_table('survived', index='sex', columns='class', margins=True)
class First Second Third All
sex        
female 0.968085 0.921053 0.500000 0.742038
male 0.368852 0.157407 0.135447 0.188908
All 0.629630 0.472826 0.242363 0.383838

這樣就可以自動獲取不同性別下船艙等級與生還率的相關信息、不同船艙等級下性別與生還率的相關信息,以及全部乘客的生還率爲 38%。margin 的標籤可以通過 margins_name 參數進行自定義,默認值是 "All"。

3.10.4 案例:美國人的生日

再來看一個有趣的例子——由美國疾病防治中心(Centers for Disease Control,CDC)提供的公開生日數據,這些數據可以從 https://raw.githubusercontent.com/jakevdp/data-CDCbirths/master/births.csv 下載。

births = pd.read_csv('births.csv')

只簡單瀏覽一下,就會發現這些數據比較簡單,只包含了不同出生日期(年月日)與性別的出生人數:

births.head()
  year month day gender births
0 1969 1 1.0 F 4046
1 1969 1 1.0 M 4440
2 1969 1 2.0 F 4454
3 1969 1 2.0 M 4548
4 1969 1 3.0 F 4548

可以用一個數據透視表來探索這份數據。先增加一列表示不同年代,看看各年代的男女出生比例:

births['decade'] = 10 * (births['year'] // 10)
births.pivot_table('births', index='decade', columns='gender', aggfunc='sum')
gender F M
decade    
1960 1753634 1846572
1970 16263075 17121550
1980 18310351 19243452
1990 19479454 20420553
2000 18229309 19106428

我們馬上就會發現,每個年代的男性出生率都比女性出生率高。如果希望更直觀地體現這種趨勢,可以用 Pandas 內置的畫圖功能將每一年的出生人數畫出來(如圖所示,詳情請參見第 4 章中用 Matplotlib 畫圖的內容):

%matplotlib inline
import matplotlib.pyplot as plt
sns.set()  # use Seaborn styles
births.pivot_table('births', index='year', columns='gender', aggfunc='sum').plot()
plt.ylabel('total births per year');

藉助一個簡單的數據透視表和 plot() 方法,我們馬上就可以發現不同性別出生率的趨勢。通過肉眼觀察,得知過去 50 年間的男性出生率比女性出生率高 5%。

深入探索

雖然使用數據透視表並不是必須的,但是通過 Pandas 的這個工具可以展現一些有趣的特徵。我們必須對數據做一點兒清理工作,消除由於輸錯了日期而造成的異常點(如 6 月 31 號)或者是缺失值(如 1999 年 6 月)。消除這些異常的簡便方法就是直接刪除異常值,可以通過更穩定的 sigma 消除法(sigma-clipping,按照正態分佈標準差劃定範圍,SciPy 中默認是四個標準差)操作來實現:

quartiles = np.percentile(births['births'], [25, 50, 75])
mu = quartiles[1]
sig = 0.74 * (quartiles[2] - quartiles[0])

最後一行是樣本均值的穩定性估計(robust estimate),其中 0.74 是指標準正態分佈的分位數間距。在 query() 方法(詳情請參見 3.13 節)中用這個範圍就可以將有效的生日數據篩選出來了:

births = births.query('(births > @mu - 5 * @sig) & (births < @mu + 5 * @sig)')

然後,將 day 列設置爲整數。這列數據在篩選之前是字符串,因爲數據集中有的列含有缺失值 'null':

births['day'] = births['day'].astype(int)

現在就可以將年月日組合起來創建一個日期索引了(詳情請參見 3.12 節),這樣就可以快速計算每一行是星期幾:

# 從年月日創建一個日期索引
births.index = pd.to_datetime(10000 * births.year +
                              100 * births.month +
                              births.day, format='%Y%m%d')

births['dayofweek'] = births.index.dayofweek

用這個索引可以畫出不同年代不同星期的日均出生數據(如圖所示):

import matplotlib.pyplot as plt
import matplotlib as mpl

births.pivot_table('births', index='dayofweek',
                    columns='decade', aggfunc='mean').plot()
plt.gca().set_xticklabels(['Mon', 'Tues', 'Wed', 'Thurs', 'Fri', 'Sat', 'Sun'])
plt.ylabel('mean births by day');

由圖可知,週末的出生人數比工作日要低很多。另外,因爲 CDC 只提供了 1989 年之前的數據,所以沒有 20 世紀 90 年代和 21 世紀的數據。

另一個有趣的圖表是畫出各個年份平均每天的出生人數,可以按照月和日兩個維度分別對數據進行分組:

births_by_date = births.pivot_table('births', 
                                    [births.index.month, births.index.day])
births_by_date.head()
    births
1 1 4009.225
2 4247.400
3 4500.900
4 4571.350
5 4603.625

這是一個包含月和日的多級索引。爲了讓數據可以用圖形表示,我們可以虛構一個年份,與月和日組合成新索引(注意日期爲 2 月 29 日時,索引年份需要用閏年,例如 2012):

births_by_date.index = [pd.datetime(2012, month, day)
                        for (month, day) in births_by_date.index]
births_by_date.head()
  births
2012-01-01 4009.225
2012-01-02 4247.400
2012-01-03 4500.900
2012-01-04 4571.350
2012-01-05 4603.625

如果只關心月和日的話,這就是一個可以反映一年中平均每天出生人數的時間序列。可以用 plot 方法將數據畫成圖(如圖 3-4 所示),從圖中可以看到一些有趣的趨勢:

# Plot the results
fig, ax = plt.subplots(figsize=(12, 4))
births_by_date.plot(ax=ax);

從圖中可以明顯看出,在美國節假日的時候,出生人數急速下降(例如美國獨立日、勞動節、感恩節、聖誕節以及新年)。這種現象可能是由於醫院放假導致的接生減少(自己在家生),而非某種自然生育的心理學效應。在 4.11.1 節會再次使用這張圖,那時將用 Matplotlib 的畫圖工具爲這張圖增加標註。

通過這個簡單的案例,你會發現許多前面介紹過的 Python 和 Pandas 工具都可以相互結合,並用於從大量數據集中獲取信息。我們將在後面的章節中介紹如何用這些工具創建更復雜的應用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章