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 工具都可以相互結合,並用於從大量數據集中獲取信息。我們將在後面的章節中介紹如何用這些工具創建更復雜的應用。