我们已经看到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_value
和dropna
同缺失数据有关,用法也很直观;我们这里不会展示使用例子。
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');
使用简单的透视表和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');
很显然,周末的出生数量低于工作日的出生数量。注意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);
图中引人注目的地方是在美国假期出生数目的下降,这更像是反映的是计划的出生而不是自然分娩。