9.透视表

我们已经看到GroupBy抽象概念如何让我们探索数据集间的关系。数据透视表是类似的操作,它在电子表格和其他操作在表格数据的程序里很常见。透视表使用简单的列方向的数据作为输入,并且将这些条目分组成可以提供多维数据总结的二维表格。透视表和GroupBy间的区别有时会引起混淆;把透视表看做GroupBy聚合的多维版本对我来说很有帮助。即拆分-应用-合并,拆分和合并都不是一维的,而是跨越二维格点。???

使用透视表的动机

作为本章的例子,我们将使用泰坦尼克上面乘客的数据库,它可以通过Seaborn库获得:

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

它包含了那次悲惨航程上乘客的众多信息,包括,性别,年纪,舱位等级,支付的票价等。

手工建立透视表

为了更多的了解这个数据,我们可以开始按照性别,生存状态,或者及其组合进行分组。如果读过之前的章节,你可能忍不住去使用GroupBy操作,让我们看一看按性别的生存率:

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

这马上给我们一些见解:总体而言,船上四分之三的女性幸存,而只有五分之一的男性生存!
这很有用,单我们可能想要更深的进一步,看看即包括性别也包括等级的存活率。使用GroupBy的方式,我们需要像这样来处理:按等级和性别分组,选择生存状态,应用均值聚合,合并结果分组,然后展开层级索引来揭示隐藏在多维数据中的内容:使用代码:

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,它简洁的处理这类二维聚合问题。

透视表语法

下面是等效的处理操作,使用的是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方式,pivot_table可读性非常强,并且产生同样的结果。如你期待的20世纪早期的跨大西洋巡航,生存率是对女性和高等级有利的。头等舱女性的生存率接近百分百(hi,Rose),而3等舱男性的生存率只有十分之一(sorry,Jack!)

多级透视表

就如GroupBy,透视表中的分组可通过许多选项来指定多层级。例如,我们想把年纪作为第三个维度,使用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, 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

结果是带有层级索引的四维聚合(参见 Hierarchical Indexing),在网格中展示各个值间的关系。

# call signature as of 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')

透视表的其它参数选项

DataFrame pivot_table方法的完全调用说明如下:

# call signature as of 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_valuedropna同缺失数据有关,用法也很直观;我们这里不会展示使用例子。
The aggfunc keyword controls what type of aggregation is applied, which is a mean by default.
aggfunc关键字控制使用哪种聚合方法,默认的是均值方法。
如同在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.0    70.0    72.0
male    67.226127   19.741782   12.661633   45.0    17.0    47.0

注意我们忽略的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标签可以通过指定the margins_name来设定,它的默认值时“All”

例子:生日数据

作为一个相对有趣的例子,让我们看一下可免费获取的美国出时数据,它由疾病控制中心提供(CDC)。数据可以在 https://raw.githubusercontent.com/jakevdp/data-CDCbirths/master/births.csv 找到。( Andrew Gelman和他的团队对这份数据由更广泛的分析; 参见 [this blog post](http://andrewgelman.com/2012/06/14/cool-ass-signal-processing-using-gaussian-processes/

# shell command to download the data:
# !curl -O https://raw.githubusercontent.com/jakevdp/data-CDCbirths/master/births.csv
births = pd.read_csv('data/births.csv')

看一眼这份数据,它相当简单--包含按时间和性别的出时日期

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

我们可以通过使用透视表开始对这份数据进行深入的了解。让我们一个十年的列,并且看看每10年男女出生情况:

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

我们马上可以看到每个10年,新生的男性数量都超过女性。为更清楚的看这个趋势,我们使用pandas内置的绘图工具来可视化每年的出生数量(关于Matplotlib绘图的讨论,参见 Introduction to 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');
03.09-Pivot-Tables_55_0.png

使用简单的透视表和plot()方法,我们可以马上看出每年按性别划分的出生趋势。看起来过去50年来男性的出生数量要比女性多5%.

更深的数据探索

虽然和透视表没有直接关系,使用pandas提供的其他工具,我们可以从这份数据中挖掘出更有趣的东西。我们必须对数据做些清理,去掉那些由错误输入(如 6月31日)和空值引起的异常值。一个简单方法就是直接删除这些异常值;我们将通过一个稳健的Sigma消波操作来实现:

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

最后一行是样本均值的稳健估计,0.74来自于高斯分布的四分位数区间(关于Sigma消波参见 "Statistics, Data Mining, and Machine Learning in Astronomy" (Princeton University Press, 2014))
通过它,我们可以使用query()方法来过滤出出时日期为异常值的行。

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

下一步,我们将day列设置为整型;它之前是字符型,因为数据集中有些列含有“null”值。

# set 'day' column to integer; it originally was a string due to nulls
births['day'] = births['day'].astype(int)

最后,我们将,日,月,年合并为一个日期索引。它会让我们快速的计算对于于每行的周工作日

# create a datetime index from the year, month, day
births.index = pd.to_datetime(10000 * births.year +
                              100 * births.month +
                              births.day, format='%Y%m%d')

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

使用这个,我们可以画出几个10年的周工作日表现:

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');
03.09-Pivot-Tables_72_0.png

很显然,周末的出生数量低于工作日的出生数量。注意1990到2000年没有数据,因为CDC从1989年开始只包含出生的月份信息。
另一个有趣的视角是按天画出出生数量。让我们首先对数据按月和日进行分组

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

结果是一个月和日多索引。为了便于绘制,让我们将这些月份和日期与虚拟年份(确保选择一个闰年,2月29日也能正确处理)关联起来,将它们转换为一个日期。

births_by_date.index = [pd.datetime(2012, month, day)
                        for (month, day) in births_by_date.index]
births_by_date.head()
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
Name: births, dtype: float64

只关注月和日,我们现在有了一个反映一年中平均出生数量的时间序列。我们使用plot方法来绘制数据。它揭示了一些有趣的趋势。

# Plot the results
fig, ax = plt.subplots(figsize=(12, 4))
births_by_date.plot(ax=ax);
03.09-Pivot-Tables_81_0.png

图中引人注目的地方是在美国假期出生数目的下降,这更像是反映的是计划的出生而不是自然分娩。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容