《python数据分析(第2版)-阿曼多.凡丹戈》读书笔记第3章-Pandas入门
第3章“Pandas入门”阐述Pandas的基本功能,其中涉及Pandas的数据结构与相应的操作。
Pandas是一个流行的开源Python程序库,其名称取panel data(面板数据,一个计量经济学的术语)与Python data analysis(Python数据分析)之意。本章将向读者介绍pandas的基本功能,其中包括Pandas的数据结构与运算。
Pandas的官方文档强调,Pandas项目名称中的字母应该全部采用小写形式,同时还约定导入这个程序库时使用的语句为import pandas as pd。
本书中,我们将尽可能地遵循这些惯例。
在本章中,我们首先安装并概要介绍Pandas,然后开始探索Pandas的两个最重要的数据结构:DataFrame和Series。最后,我们将学习如何对存储在这些数据结构中的数据进行类似SQL这样的运算,还举例说明包括时间序列例程在内的统计学工具。本章涉及的主题如下。
- Pandas的安装与概览
- Pandas的数据结构:DataFrame
- Pandas的数据结构:Series
- 利用Pandas查询数据
- 利用Pandas的DataFrames进行统计计算
- 利用Pandas的DataFrames聚合数据
- DataFrames的连接(joining)与附加(appending)操作
- DataFrames的串联(concatenating)操作
- 处理缺失数据问题
- 处理日期数据
- 数据透视表
3.1Pandas的安装与概览
对于Pandas来说,最小的依赖项集合如下。
NumPy:这是一个处理数值数组的基础软件包,我们已经在前面的章节介绍过它的安装方法和简单用法。
Python-dateutil:这是一个专门用来处理日期数据的程序库。
Pytz:这是一个处理时区问题的程序库。
上面列出的是最低限度的依赖项,如果想更加全面地了解可选依赖项,请访问https://pandas.pydata.org/pandas-docs/stable/getting_started/install.html页面。我们不仅能用PyPI的pip或者easy_install命令来安装Pandas,而且可以使用已经编译好的二进制形式的安装程序。此外,我们还可以借助于操作系统的程序包管理器,甚至可以利用源代码来安装Pandas。如果我们喜欢使用二进制形式的安装程序,可以从Pandas官网下载。
用pip程序安装Pandas的命令如下。
pip3 install pandas
3.2Pandas数据结构之DataFrame
Pandas的DataFrame数据结构是一种带标签的二维对象,与Excel的电子表格或者关系型数据库的数据表非常相似。顺便说一下,DataFrame的概念最初发源于R程序语言。我们可以用下列方式来创建DataFrame。
- 使用另一个DataFrame创建DataFrame。
- 使用具有二维形状的NumPy数组或者数组的复合结构来生成DataFrame。
- 类似地,可以用Pandas的另外一种数据结构Series来创建DataFrame。关于Series,后文将有所介绍。
- DataFrame也可以从类似CSV之类的文件来生成。
- 使用一维NumPy数组、列表、字典或Pandas Series之类的一维结构的字典来生成DataFrame。
下面以数据为例,来说明Datarame。注意,原始的数据文件相当大,而且有很多列,所以我们将使用编辑后的仅保留前9列的一个文件作为替代。这个文件可以从本书的代码包中找到,文件的全称为WHO_first9cols.csv。下面是该文件包括标题在内的前两行内容。
接下来,我们开始考察Pandas的DataFrame及其各种属性。
(1) 首先,将数据文件载入DataFrame并显示其内容。
1 From pandas.io.parsers import read_csv 2 Df = read_csv(“WHO_first9cols.csv”) 3 Print(“Dataframe”, df)
这里显示的是该DataFrame的摘要信息。如果完整显示的话会很长,所以我们只是截取了前面几行内容。
Dataframe: Country ... Population (in thousands) total 0 Afghanistan ... 26088.0 1 Albania ... 3172.0 2 Algeria ... 33351.0 3 Andorra ... 74.0 4 Angola ... 16557.0 .. ... ... ... 197 Vietnam ... 86206.0 198 West Bank and Gaza ... NaN 199 Yemen ... 21732.0 200 Zambia ... 11696.0 201 Zimbabwe ... 13228.0 [202 rows x 9 columns]
(2)DataFrame有一个属性,以元组的形式来存放DataFrame的形状数据,这与ndarray非常类似。我们可以查询一个DataFrame的行数,具体方法如下。
1 Print(“Shape”, df.shape) 2 Print(“Length”, len(df))
这里得到的数字与上一步的打印输出一致。
Shape (202, 9)
Length 202
(3)下面我们通过其他属性来考察各列的标题与数据类型,具体如下。
1 Print(“Column Headers”, df.columns) 2 Print(“Data types”, df.dtypes)
我们可以用一个专用的数据结构来容纳列标题(column headers)。
Column Headers: Index(['Country', 'CountryID', 'Continent', 'Adolescent fertility rate (%)', 'Adult literacy rate (%)', 'Gross national income per capita (PPP international $)', 'Net primary school enrolment ratio female (%)', 'Net primary school enrolment ratio male (%)', 'Population (in thousands) total'], dtype='object')
该数据类型的输出结果如图3-1所示。
Data types: Country object CountryID int64 Continent int64 Adolescent fertility rate (%) float64 Adult literacy rate (%) float64 Gross national income per capita (PPP international $) float64 Net primary school enrolment ratio female (%) float64 Net primary school enrolment ratio male (%) float64 Population (in thousands) total float64 dtype: object
(4)Pandas的DataFrame带有一个索引,类似于关系型数据库中数据表的主键(primary key)。对于这个索引,我们既可以手动规定,也可以让pandas自动创建。访问索引时,使用相应的属性即可,具体方法如下。
1 Print(“Index”, df.index)
利用索引可以迅速搜查数据项,正如书籍的索引一样。就本例而言,这个索引实际上是对数组的一种封装,它起始于0,然后以1为单位逐行递增。
Index:
RangeIndex(start=0, stop=202, step=1)
(5)有时我们希望遍历DataFrame的基础数据,如果使用pandas的迭代器,遍历列值的效率可能会很低。更好的解决方案是从基础的NumPy数组中提取这些数值,然后进行相应的处理。不过,pandas的DataFrame提供了一个属性,可以在这方面为我们提供帮助。
1 Print(“Values”, df.values)
注意,在输出中,一些非数字的数值被标为“nan”,这通常是由输入数据文件中的空字段引起的。
Values [[‘Afghanistan’ 1 1 …, nan nan 26088.0] [‘Albania’ 2 2 …, 93.0 94.0 3172.0] [‘Algeria’ 3 3 …, 94.0 96.0 33351.0] …, [‘Yemen’ 200 1 …, 65.0 85.0 21732.0] [‘Zambia’ 201 3 …, 94.0 90.0 11696.0] [‘Zimbabwe’ 202 3 …, 88.0 87.0 13228.0]]
上述展示的示例代码取自本书代码包中的ch-03.ipynb文件。
3.3Pandas数据结构之Series
Pandas的Series数据结构是由不同类型的元素组成的一维数组,该数据结构也具有标签。我们可以通过下列方式来创建Pandas的Series数据结构。
- 使用Python的字典来创建Series
- 使用NumPy数组来创建Series
- 使用单个标量值来创建Series
创建Series数据结构时,我们可以向构造函数递交一组轴标签,这些标签通常称为索引,是一个可选参数。默认情况下,如果使用NumPy数组作为输入数据,那么Pandas会将索引值从0开始自动递增。如果传递给构造函数的数据是一个Python字典,那么这个字典的键会经排序后变成相应的索引。如果输入数据是一个标量值,那么就需要由我们来提供相应的索引。索引中的每一个新值都要输入一个标量值。Pandas的Series和DataFrame数据类型接口的特征和行为是从NumPy数组和Python字典那里借用来的,如切片、通过键查找以及向量化运算等。对一个DataFrame列执行查询操作时,会返回一个Series数据结构。关于这一点以及Series的其他特点,我们会以3.2节中的数据为例进行说明,所以需要再次加载CSV文件。
(1)首先,我们选中输入文件中的第一列,即Country列;然后,显示这个对象在局部作用域中的类型。
1 Country_col = df[“Country”] 2 Print(“Type df”, type(df)) 3 Print(“Type country col”, type(country_col))
如今我们可以肯定,当选中DataFrame的一列时,得到的是一个Series型的数据,代码如下。
Type df <class ‘pandas.core.frame.DataFrame’> Type country col <class ‘pandas.core.series.Series’>
Tips:如果需要,我们打开一个Python或者IPython shell,导入Pandas后就可以通过dir()函数来查看上述类型提供的各种函数和属性了,这时显示的函数列表将会更加全面。
(2)Pandas的Series数据结构不仅共享了DataFrame的一些属性,还另外提供了与名称有关的一个属性。下面通过代码做进一步的探索。
1 Print(“Series shape”, country_col.shape) 2 Print(“Series index”, country_col.index) 3 Print(“Series values”, country_col.values) 4 Print(“Series name”, country_col.name)
输出内容(为了节约版面,这里仅截取了部分内容)如下。
Series shape (202,) Series index Int64Index([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, …], dtype=’int64’) Series values [‘Afghanistan’ … ‘Vietnam’ ‘West Bank and Gaza’ ‘Yemen’ ‘Zambia’ ‘Zimbabwe’] Series name Country
(3)为了演示Series的切片功能,这里以截取Series变量Country中的最后两个国家为例进行说明,具体代码如下。
1 Print(“Last 2 countries”, country_col[-2:]) 2 Print(“Last 2 countries type”, type(country_col[-2:]))
就像我们看到的那样,利用切片技术,我们得到了一个新的Series数据对象。
Last 2 countries 200 Zambia 201 Zimbabwe Name: Country, dtype: object Last 2 countries type <class ‘pandas.core.series.Series’>
(4)NumPy的函数同样适用于Pandas的DataFrame和Series数据结构。例如,可以使用NumPy的sign()函数来获得数字的符号:正数返回1,负数返回−1,零值返回0。下面将这个函数应用到前面的DataFrame数据及其最后一列上面,即数据集中各国的人口数量。
1 Last_col = df.columns[-1] 2 Print(“Last df column signs:\n”, last_col, 3 Np.sign(df[last_col]), “\n”)
为了节约版面,这里对输出内容进行了一定的删减,代码如下。
Last df column signs Population (in thousands) total 0 1 1 1 2 [TRUNCATED] 198 NaN 199 1 200 1 201 1 Name: Population (in thousands) total, Length: 202, dtype: Float64
Tips:注意,198 号索引对应的人口值为“NaN”,相应的数据项如下。West Bank and Gaza,199,1,,,,,,
我们可以在DataFrames、Series和NumPy数组之间进行各种类型的数值运算。举例来说,如果得到pandas的Series数据中的一个基础NumPy数组,然后把它从该Series中减掉,这时我们可能希望得到下面两种结果:
- 一个数组,各个元素以零填充,而且至少有一个 NaN(前面的步骤中已经提到过NaN);
- 一个元素全部为零值的数组。
对于NumPy函数来说,涉及NaN的大部分运算都会生成NaN。下面我们使用一个IPython会话进行说明。
1 np.sum([0, np.nan])
Out: nan
利用下面的代码执行减法运算。
1 Print np.sum(df[last_col] – df[last_col].values)
得到的数据与第二种类型的结果相符。
0.0
本节涉及的代码取自本书代码包中的ch-03.ipynb文件。
3.4利用Pandas查询数据
因为Pandas的DataFrame的结构类似于关系型数据库,所以从DataFrame读取数据可以看作是一种查询操作。下面的例子中,我们将会从Quandl检索年度太阳黑子数据。为此,我们既可以使用Quandl API,也可以亲自动手从http://www.quandl.com/SIDC/ SUNSPOTS_A-Sunspot-Numbers-Annual页面以CSV文件的形式下载数据。如果想安装API,我们可以从Quandl网站下载相应的安装程序,或者运行以下命令。
$ pip3 install Quandl
提示:Quandl的API可以免费使用,但是有一个限制,即每天最多调用50次。如果超过规定的调用次数,则需要申请身份验证密钥。不过,这里的代码无需使用密钥。如果需要使用密钥或读取下载的CSV文件,对这里的代码稍作修改即可。如果有困难,请参考第1章中从何处寻求帮助和参考资料部分,或者到官方指定的Python文档中查找解决方案。
话不多说,下面来看如何利用Pandas的DataFrame来查询数据。
(1)下载数据,导入Quandl API后,可以像下面这样来下载数据。
1 Import quandl 2 # Data from http://www.quandl.com/SIDC/SUNSPOTS_A-Sunspot-Numbers-Annual 3 # PyPi url https://pypi.python.org/pypi/Quandl 4 Sunspots = quandl.get(“SIDC/SUNSPOTS_A”)
(2)head()和 tail()这两个函数的作用类似于UNIX系统中同名的两个命令,即选取DataFrame的前n个和后n个数据记录,其中n是一个整型参数。
1 print(“Head 2”, sunspots.head(2) ) 2 print(“Tail 2”, sunspots.tail(2))
下面给出太阳黑子数据的前两条和后两条数据的内容(为了保持版面简洁起见,没有在这里显示所有列;不过在运行代码的时候,输出中将含有数据集的所有列)。
Head 2: Yearly Mean Total Sunspot Number Yearly Mean Standard Deviation \ Date 1700-12-31 8.3 NaN 1701-12-31 18.3 NaN Number of Observations Definitive/Provisional Indicator Date 1700-12-31 NaN 1.0 1701-12-31 NaN 1.0
Tail 2: Yearly Mean Total Sunspot Number Yearly Mean Standard Deviation \ Date 2018-12-31 7.0 1.1 2019-12-31 3.6 0.5 Number of Observations Definitive/Provisional Indicator Date 2018-12-31 12611.0 1.0 2019-12-31 12401.0 0.0
注意,我们有且只有一列数据来存放每年太阳黑子的数量,而日期属于DataFrame索引的一部分。
(3)下面用最近的日期来查询最近一年太阳黑子的相关数据。
1 Last_date = sunspots.index[-1] 2 Print(“Last value”, sunspots.loc[last_date])
我们可以将下面的输出内容与前面得到的结果进行比对。
Last value: Yearly Mean Total Sunspot Number 3.6 Yearly Mean Standard Deviation 0.5 Number of Observations 12401.0 Definitive/Provisional Indicator 0.0 Name: 2019-12-31 00:00:00, dtype: float64
(4)下面介绍如何通过YYYYMMDD格式的日期字符串来查询日期,具体如下。
1 Print(“Values slice by date:\n”, sunspots[“20020101”:“20131231”])
以下结果为2002-2013年的数据记录。
Values slice by date: Yearly Mean Total Sunspot Number Yearly Mean Standard Deviation \ Date 2002-12-31 163.6 9.8 2003-12-31 99.3 7.1 2004-12-31 65.3 5.9 2005-12-31 45.8 4.7 2006-12-31 24.7 3.5 2007-12-31 12.6 2.7 2008-12-31 4.2 2.5 2009-12-31 4.8 2.5 2010-12-31 24.9 3.4 2011-12-31 80.8 6.7 2012-12-31 84.5 6.7 2013-12-31 94.0 6.9 Number of Observations Definitive/Provisional Indicator Date 2002-12-31 6588.0 1.0 2003-12-31 7087.0 1.0 2004-12-31 6882.0 1.0 2005-12-31 7084.0 1.0 2006-12-31 6370.0 1.0 2007-12-31 6841.0 1.0 2008-12-31 6644.0 1.0 2009-12-31 6465.0 1.0 2010-12-31 6328.0 1.0 2011-12-31 6077.0 1.0 2012-12-31 5753.0 1.0 2013-12-31 5347.0 1.0
(5)索引列表也可用于查询,代码如下。
1 Print(“Slice from a list of indices:\n”, sunspots.iloc[[2, 4,-4, -2]])
上述代码会选择下列各行。
Slice from a list of indices: Yearly Mean Total Sunspot Number Yearly Mean Standard Deviation \ Date 1702-12-31 26.7 NaN 1704-12-31 60.0 NaN 2016-12-31 39.8 3.9 2018-12-31 7.0 1.1 Number of Observations Definitive/Provisional Indicator Date 1702-12-31 NaN 1.0 1704-12-31 NaN 1.0 2016-12-31 9940.0 1.0 2018-12-31 12611.0 1.0
(6)要想选择标量值,有两种方法,这里给出的是速度明显占优势的第二种方法。它需要两个整数作为参数,其中第一个整数表示行,第二个整数表示列。
1 Print(“Scalar with Iloc:”, sunspots.iloc[0, 0]) 2 Print(“Scalar with iat”, sunspots.iat[1, 0])
上面的代码将数据集的第一个值和第二个值作为标量返回。
Scalar with Iloc: 8.3
Scalar with iat 18.3
(7)查询布尔型变量的方法与SQL的Where子句非常接近。下面的代码将查询大于算术平均值的各个数值。注意,在整个DataFrame中进行查询与在单列上进行查询是有区别的。
1 Print(“Boolean selection”, sunspots[sunspots >Sunspots.mean()]) 2 Print(“Boolean selection with column label:\n”,Sunspots[sunspots[‘Number of Observations’] > sunspots[‘NumberOf Observations’].mean()])
显著的区别在于,第一个查询操作得到的是所有数据行,其中与条件不符的行将被赋予NaN值,第二个查询操作返回的只是其值大于平均值的那些行。
Boolean selection: Yearly Mean Total Sunspot Number Yearly Mean Standard Deviation \ Date 1700-12-31 NaN NaN 1701-12-31 NaN NaN 1702-12-31 NaN NaN 1703-12-31 NaN NaN 1704-12-31 NaN NaN ... ... ... 2015-12-31 NaN NaN 2016-12-31 NaN NaN 2017-12-31 NaN NaN 2018-12-31 NaN NaN 2019-12-31 NaN NaN Number of Observations Definitive/Provisional Indicator Date 1700-12-31 NaN 1.0 1701-12-31 NaN 1.0 1702-12-31 NaN 1.0 1703-12-31 NaN 1.0 1704-12-31 NaN 1.0 ... ... ... 2015-12-31 8903.0 1.0 2016-12-31 9940.0 1.0 2017-12-31 11444.0 1.0 2018-12-31 12611.0 1.0 2019-12-31 12401.0 NaN [320 rows x 4 columns]
Boolean selection with column label: Yearly Mean Total Sunspot Number Yearly Mean Standard Deviation \ Date 1981-12-31 198.9 13.1 1982-12-31 162.4 12.1 1983-12-31 91.0 7.6 1984-12-31 60.5 5.9 1985-12-31 20.6 3.7 1986-12-31 14.8 3.5 1987-12-31 33.9 3.7 1988-12-31 123.0 8.4 1989-12-31 211.1 12.8 1990-12-31 191.8 11.2 1991-12-31 203.3 12.7 1992-12-31 133.0 8.9 1993-12-31 76.1 5.8 1994-12-31 44.9 4.4 1995-12-31 25.1 3.7 1996-12-31 11.6 3.1 1997-12-31 28.9 3.6 1998-12-31 88.3 6.6 1999-12-31 136.3 9.3 2000-12-31 173.9 10.1 2001-12-31 170.4 10.5 2002-12-31 163.6 9.8 2003-12-31 99.3 7.1 2004-12-31 65.3 5.9 2005-12-31 45.8 4.7 2006-12-31 24.7 3.5 2007-12-31 12.6 2.7 2008-12-31 4.2 2.5 2009-12-31 4.8 2.5 2010-12-31 24.9 3.4 2011-12-31 80.8 6.7 2012-12-31 84.5 6.7 2013-12-31 94.0 6.9 2014-12-31 113.3 8.0 2015-12-31 69.8 6.4 2016-12-31 39.8 3.9 2017-12-31 21.7 2.5 2018-12-31 7.0 1.1 2019-12-31 3.6 0.5 Number of Observations Definitive/Provisional Indicator Date 1981-12-31 3049.0 1.0 1982-12-31 3436.0 1.0 1983-12-31 4216.0 1.0 1984-12-31 5103.0 1.0 1985-12-31 5543.0 1.0 1986-12-31 5934.0 1.0 1987-12-31 6396.0 1.0 1988-12-31 6556.0 1.0 1989-12-31 6932.0 1.0 1990-12-31 7108.0 1.0 1991-12-31 6932.0 1.0 1992-12-31 7845.0 1.0 1993-12-31 8010.0 1.0 1994-12-31 8524.0 1.0 1995-12-31 8429.0 1.0 1996-12-31 7614.0 1.0 1997-12-31 7294.0 1.0 1998-12-31 6353.0 1.0 1999-12-31 6413.0 1.0 2000-12-31 5953.0 1.0 2001-12-31 6558.0 1.0 2002-12-31 6588.0 1.0 2003-12-31 7087.0 1.0 2004-12-31 6882.0 1.0 2005-12-31 7084.0 1.0 2006-12-31 6370.0 1.0 2007-12-31 6841.0 1.0 2008-12-31 6644.0 1.0 2009-12-31 6465.0 1.0 2010-12-31 6328.0 1.0 2011-12-31 6077.0 1.0 2012-12-31 5753.0 1.0 2013-12-31 5347.0 1.0 2014-12-31 5273.0 1.0 2015-12-31 8903.0 1.0 2016-12-31 9940.0 1.0 2017-12-31 11444.0 1.0 2018-12-31 12611.0 1.0 2019-12-31 12401.0 0.0
以上示例代码取自本书代码包中的ch_03.ipynb文件。
3.5利用Pandas的DataFrame进行统计计算
Pandas的DataFrame数据结构为我们提供了若干统计函数,表3-1给出了部分方法及其简要说明。
- Describe:这个方法将返回描述性统计信息
- Count:这个方法将返回非NaN数据项的数量
- Mad:这个方法用于计算平均绝对偏差(mean absolute deviation),即类似于标准差的一个有力统计工具
- Median:这个方法将返回中位数,等价于第50位百分位数的值
- Min:这个方法将返回最小值
- Max:这个方法将返回最大值
- Mode:这个方法将返回众数(mode),即一组数据中出现次数最多的变量值
- Std:这个方法将返回表示离散度(dispersion)的标准差,即方差的平方根
- Var:这个方法将返回方差
- Skew:这个方法用来返回偏态系数(skewness),该系数表示的是数据分布的对称程度
- Kurt:这个方法将返回峰态系数(kurtosis),该系数用来反映数据分布曲线顶端尖峭或扁平程度
下面使用上例中的数据来演示这些统计函数的使用方法。如果读者对完整的代码感兴趣,请参考本书代码包中的ch-03.ipynb文件。演示代码如下。
1 Import quandl 2 # Data from http://www.quandl.com/SIDC/SUNSPOTS_A-Sunspot-Numbers-Annual 3 # PyPi url https://pypi.python.org/pypi/Quandl 4 Sunspots = quandl.get(“SIDC/SUNSPOTS_A”) 5 Print(“Describe”, sunspots.describe(),”\n”) 6 Print(“Non NaN observations”, sunspots.count(),”\n”) 7 Print(“MAD”, sunspots.mad(),”\n”) 8 Print(“Median”, sunspots.median(),”\n”) 9 Print(“Min”, sunspots.min(),”\n”) 10 Print(“Max”, sunspots.max(),”\n”) 11 Print(“Mode”, sunspots.mode(),”\n”) 12 Print(“Standard Deviation”, sunspots.std(),”\n”) 13 Print(“Variance”, sunspots.var(),”\n”) 14 Print(“Skewness”, sunspots.skew(),”\n”) 15 Print(“Kurtosis”, sunspots.kurt(),”\n”)
以上脚本的运行结果如下所示。
Describe Yearly Mean Total Sunspot Number Yearly Mean Standard Deviation \ count 320.000000 202.000000 mean 78.735000 7.910891 std 62.065761 3.866630 min 0.000000 0.500000 25% 24.575000 4.625000 50% 65.550000 7.600000 75% 115.475000 10.375000 max 269.300000 19.100000 Number of Observations Definitive/Provisional Indicator count 202.000000 320.000000 mean 1626.356436 0.996875 std 2768.152562 0.055902 min 150.000000 0.000000 25% 365.000000 1.000000 50% 365.000000 1.000000 75% 366.000000 1.000000 max 12611.000000 1.000000 Non NaN observations Yearly Mean Total Sunspot Number 320 Yearly Mean Standard Deviation 202 Number of Observations 202 Definitive/Provisional Indicator 320 dtype: int64 MAD Yearly Mean Total Sunspot Number 50.995250 Yearly Mean Standard Deviation 3.174904 Number of Observations 2067.406921 Definitive/Provisional Indicator 0.006230 dtype: float64 Median Yearly Mean Total Sunspot Number 65.55 Yearly Mean Standard Deviation 7.60 Number of Observations 365.00 Definitive/Provisional Indicator 1.00 dtype: float64 Min Yearly Mean Total Sunspot Number 0.0 Yearly Mean Standard Deviation 0.5 Number of Observations 150.0 Definitive/Provisional Indicator 0.0 dtype: float64 Max Yearly Mean Total Sunspot Number 269.3 Yearly Mean Standard Deviation 19.1 Number of Observations 12611.0 Definitive/Provisional Indicator 1.0 dtype: float64 Mode Yearly Mean Total Sunspot Number Yearly Mean Standard Deviation \ 0 18.3 9.2 Number of Observations Definitive/Provisional Indicator 0 365.0 1.0 Standard Deviation Yearly Mean Total Sunspot Number 62.065761 Yearly Mean Standard Deviation 3.866630 Number of Observations 2768.152562 Definitive/Provisional Indicator 0.055902 dtype: float64 Variance Yearly Mean Total Sunspot Number 3.852159e+03 Yearly Mean Standard Deviation 1.495083e+01 Number of Observations 7.662669e+06 Definitive/Provisional Indicator 3.125000e-03 dtype: float64 Skewness Yearly Mean Total Sunspot Number 0.812243 Yearly Mean Standard Deviation 0.525985 Number of Observations 1.998940 Definitive/Provisional Indicator -17.888544 dtype: float64 Kurtosis Yearly Mean Total Sunspot Number -0.125983 Yearly Mean Standard Deviation -0.250271 Number of Observations 2.951176 Definitive/Provisional Indicator 320.000000 dtype: float64
3.6利用Pandas的DataFrame实现数据聚合
数据聚合(data aggregation)是关系型数据库中比较常用的一个术语。使用数据库时,我们可以利用查询操作对各列或各行中的数据进行分组,这样就可以针对其中的每一组数据进行各种不同的操作了。实际上,Pandas的DataFrame数据结构也为我们提供了类似的功能。我们可以把生成的数据保存到Python字典中,然后利用这些数据来创建一个Pandas DataFrame,接下来就可以练习Pandas提供的聚合功能了。
(1) 我们为NumPy的随机数生成器指定种子,确保重复运行程序时生成的数据不会走样。该数据有4列,分别是:
- Weather(一个字符串)
- Food(一个字符串)
- Price(一个随机浮点数)
- Number(1~9之间的一个随机整数)
假设有一些客户消费调查、天气与市场定价方面的资料,这里要做的是计算平均价格并跟踪样本的大小及参数。
1 import pandas as pd 2 from numpy.random import seed 3 from numpy.random import rand 4 from numpy.random import randint 5 import numpy as np 6 7 seed(42) 8 9 df = pd.DataFrame({'Weather' : ['cold', 'hot', 'cold', 'hot', 10 'cold', 'hot', 'cold'], 11 'Food' : ['soup', 'soup', 'icecream', 'chocolate', 12 'icecream', 'icecream', 'soup'], 13 'Price' : 10 * rand(7), 'Number' : randint(1, 9)}) 14 15 print(df)
我们将得到下面的结果。
Weather Food Price Number
0 cold soup 3.745401 8
1 hot soup 9.507143 8
2 cold icecream 7.319939 8
3 hot chocolate 5.986585 8
4 cold icecream 1.560186 8
5 hot icecream 1.559945 8
6 cold soup 0.580836 8
Tips:注意,列标签是按照Python字典各个键的词汇顺序进行排列的。所谓的词汇或者词典顺序,就是按照字符串中各个字符的字母表顺序进行排序。
(2)通过Weather列为数据分组,然后遍历各组数据,代码如下。
1 weather_group = df.groupby('Weather') 2 3 i = 0 4 5 for name, group in weather_group: 6 i = i + 1 7 print("Group", i, name) 8 print(group)
我们把天气状况分为两种,即热天与冷天,据此可以把数据分为两组。
Group 1 cold Weather Food Price Number 0 cold soup 3.745401 8 2 cold icecream 7.319939 8 4 cold icecream 1.560186 8 6 cold soup 0.580836 8 Group 2 hot Weather Food Price Number 1 hot soup 9.507143 8 3 hot chocolate 5.986585 8 5 hot icecream 1.559945 8
(3)变量Weather_group是一种特殊的Pandas对象,可由groupby()生成。这个对象为我们提供了聚合函数,下面我们展示它的使用方法。
1 print("Weather group first\n", weather_group.first()) 2 print("Weather group last\n", weather_group.last()) 3 print("Weather group mean\n", weather_group.mean())
以上代码将输出各组数据的第1行内容、第2行内容以及各组的平均值,显示如下。
Weather group first Food Price Number Weather cold soup 3.745401 8 hot soup 9.507143 8 Weather group last Food Price Number Weather cold soup 0.580836 8 hot icecream 1.559945 8 Weather group mean Price Number Weather cold 3.301591 8 hot 5.684558 8
(4)恰如利用数据库的查询操作那样,我们也可以针对多列进行分组。
此后,我们便可以利用groups属性来了解所生成的数据组以及每一组包含的行数。
1 wf_group = df.groupby(['Weather', 'Food']) 2 print("WF Groups", wf_group.groups)
针对天气数据和食物数据的每一种可能的组合,都会为其生成一个新的数据组。每一行中的各个数据项都可以通过索引值引用,具体如下。
WF Groups {('cold', 'icecream'): Int64Index([2, 4], dtype='int64'), ('cold', 'soup'): Int64Index([0, 6], dtype='int64'),
('hot', 'chocolate'): Int64Index([3], dtype='int64'), ('hot', 'icecream'): Int64Index([5], dtype='int64'),
('hot', 'soup'): Int64Index([1], dtype='int64')}
(5)通过agg()方法,可以对数据组施加一系列的NumPy函数。
1 Print(“WF Aggregated\n”, wf_group.agg([np.mean, np.median]))
显而易见,我们也可以施加更多的函数。不过这样做的结果是,它的输出结果会比下面的输出结果更乱。
WF Aggregated Price Number mean median mean median Weather Food cold icecream 4.440063 4.440063 8 8 soup 2.163119 2.163119 8 8 hot chocolate 5.986585 5.986585 8 8 icecream 1.559945 1.559945 8 8 soup 9.507143 9.507143 8 8
完整的数据聚合示例代码请参考data_aggregation.py文件,这个文件位于本书的代码包中。
3.7DataFrame的串联与附加操作
数据库的数据表有内部连接和外部连接两种连接操作类型。实际上,Pandas 的DataFrame 也有类似的操作,因此我们也可以对数据行进行串联和附加。我们将使用前面章节中的DataFrame来练习数据行的串联与附加操作。我们首先选中前面3行数据。
1 Print(“df :3\n”, df[:3])
下面我们来看输出的是否为前3行的内容。
df :3
Weather Food Price Number
0 cold soup 3.745401 8
1 hot soup 9.507143 8
2 cold icecream 7.319939 8
函数concat()的作用是串联DataFrame,例如可以把一个由3行数据组成的DataFrame与其他数据行串接,以便重建原DataFrame。
1 Print(“Concat Back together\n”, pd.concat([df[:3], df[3:]]))
串联后的效果如下。
Concat Back together
Weather Food Price Number
0 cold soup 3.745401 8
1 hot soup 9.507143 8
2 cold icecream 7.319939 8
3 hot chocolate 5.986585 8
4 cold icecream 1.560186 8
5 hot icecream 1.559945 8
6 cold soup 0.580836 8
为了追加数据行,可以使用append()函数。
1 Print(“Appending rows\n”, df[:3].append(df[5:]))
我们得到的DataFrame的前3行数据来自原DataFrame,后面两行数据是附加上的。
Appending rows
Weather Food Price Number
0 cold soup 3.745401 8
1 hot soup 9.507143 8
2 cold icecream 7.319939 8
5 hot icecream 1.559945 8
6 cold soup 0.580836 8
3.8连接DataFrames
为演示连接操作,我们要用到两个CSV文件,dest.csv与tips.csv文件。假设我们正在运营一家出租车公司,每当乘客在目的地下车后,我们就需要向dest.csv文件增加一行数据,内容为出租车司机的员工编号与目的地。
EmpNr,Dest5,The Hague3,Amsterdam9,Rotterdam
有时,出租车司机会收到一些小费,因此我们希望把它登记到tips.csv文件中(这听起来好像不太现实,如果读者有好的案例,请随时提供给我们)。
EmpNr,Amount5,109,57,2.5
Pandas提供的merge()函数或DataFrame的join()实例方法都能实现类似数据库的连接操作功能。默认情况下,join()实例方法会按照索引进行连接。不过,有时候这不符合我们的要求。使用关系型数据库查询语言(SQL)时,可以进行内部连接、左外连接、右外连接与完全外部连接等操作。
Tips:对于内部连接,它会从两个数据表中选取数据,只要两个表中连接条件规定的列上存在相匹配的值,相应的数据就会被组合起来。对于外部连接,因为不要求进行匹配处理,所以将返回更多的数据。关于连接操作的进一步介绍,读者可以参考维基百科页面。
虽然Pandas支持所有的这些连接类型,限于篇幅,这里只介绍内部连接与完全外部连接。
我们用merge()函数按照员工编号进行连接处理,代码如下。
1 Print(“Merge() on key\n”, pd.merge(dests, tips, on=’EmpNr’))
下面给出内部连接的结果。
Merge() on key
EmpNr Dest Amount
0 5 The Hague 10.0
1 9 Rotterdam 5.0
用join()方法执行连接操作时,需要使用后缀来指示左操作对象和右操作对象。
1 print("Dests join() tips\n", dests.join(tips, lsuffix='Dest', rsuffix='Tips')
这个方法会连接索引值,因此得到的结果与SQL内部连接会有所不同。
Dests join() tips
EmpNrDest Dest EmpNrTips Amount
0 5 The Hague 5 10.0
1 3 Amsterdam 9 5.0
2 9 Rotterdam 7 2.5
用merge()执行内部连接时,更显式的方法如下。
1 print("Inner join with merge()\n", pd.merge(dests, tips, how='inner'))
输出内容如下。
Inner join with merge()
EmpNr Dest Amount
0 5 The Hague 10.0
1 9 Rotterdam 5.0
只要稍作修改,就可以变成完全外部连接。
1 Print(“Outer join\n”, pd.merge(dests, tips, how=’outer’))
此外部连接操作增添了几行带有NaN值的数据。
Outer join EmpNr Dest Amount 0 5 The Hague 10.0 1 3 Amsterdam NaN 2 9 Rotterdam 5.0 3 7 NaN 2.5
如果使用关系型数据库的查询操作,这些数据都会被设为NULL。以上演示代码取自本书代码包中的ch – 03.ipynb文件:
3.9处理缺失数据问题
无论我们喜不喜欢,都会经常在数据记录中遇到空字段,所以我们最好接受这个现实并且学习如何通过一种可行的方式来解决这种问题。现实中的数据不仅有遗漏的现象,而且其中的某些数据还可能是错误的,因为所有的测量设备都难免会出现故障。对于Pandas来说,它会把缺失的数值标为NaN,表示None;还有一个类似的符号是NaT,不过,它代表的是datetime64型对象。对NaN这个数值进行算术运算时,得到的结果还是NaN。如果描述性统计学方法遇到这种值,如求和与求均值时,结果就不同了。就像我们在前面的例子中看到的那样,有时NaN被当成零值来进行处理。然而,在诸如求和之类的运算过程中,如果所有的数值都是NaN,返回的结果仍然是NaN。进行聚合操作时,我们组合的列内的NaN值会被忽略。下面我们会重新把WHO_first9cols.csv加载到一个DataFrame中。注意,这个文件中含有许多空白字段。这里只选取前3行数据,其中包括Country列与Net primary school enrolment ratio male (%)列的标题,具体代码如下。
1 df = pd.read_csv('WHO_first9cols.csv') 2 # Select first 3 rows of country and Net primary school enrolment ratio male (%) 3 df = df[['Country', df.columns[-2]]][:2] 4 print("New df\n", df)
这样,我们就得到一个包含两个NaN值的DataFrame数据结构,具体内容如下。
New df Country Net primary school enrolment ratio male (%) 0 Afghanistan NaN 1 Albania 94.0
Pandas的isnull()函数可以帮我们检查缺失的数据,使用方法如下。
1 Print(“Null Values\n”, pd.isnull(df))
对于我们的DataFrame,它的输出如下。
Null Values Country Net primary school enrolment ratio male (%) 0 False True 1 False False
如果要统计每一列中NaN值的数量,只要对isnull()函数返回的布尔值进行求和即可。这种方法之所以奏效,是因为在求和的过程中True值被视为1,而False值被视为0来对待。
Total Null Values Country 0 Net primary school enrolment ratio male (%) 1 dtype: int64
类似地,可以用DataFrame的notnull()方法来考察非缺失数据。
1 Print(“Not Null Values\n”, df.notnull())
Notnull()方法的返回结果与isnull()函数的返回结果正好相反。
Not Null Values Country Net primary school enrolment ratio male (%) 0 True False 1 True True
如果一个DataFrame含有NaN值,那么将这个DataFrame中的值都乘2后,结果还是含有NaN值,因为“乘2”是一种算术运算。
1 Print(“Last Column Doubled\n”, 2 * df[df.columns[-1]])
下面用2乘以含有数值的最后一列(字符串乘以2表示重复该字符串)。
Last Column Doubled 0 NaN 1 188.0 Name: Net primary school enrolment ratio male (%), dtype: float64
然而,如果加一个NaN值,NaN值就会大获全胜。
1 Print(“Last Column plus NaN\n”, df[df.columns[-1]] + np.nan)
如你所见,现在变成NaN值的天下了。
Last Column plus NaN 0 NaN 1 NaN Name: Net primary school enrolment ratio male (%), dtype: float6
通过fillna()方法,可以用一个标量值(如0)来替换缺失数据,尽管有时可以用0替换缺失数据,但是事情并不总是如此。
1 Print(“Zero filled\n”, df.fillna(0))
执行上面的代码后,NaN值就会被0替换。
Zero filled Country Net primary school enrolment ratio male (%) 0 Afghanistan 0.0 1 Albania 94.0
本节代码选自本书代码包中的ch-03.ipynb文件。
3.10处理日期数据
日期数据处理起来比较复杂,如Y2K问题、悬而未决的2038年问题以及时区问题等。虽然日期数据的处理很棘手,但是在处理时间序列数据时,这种数据是必不可少的。好在Pandas可以帮我们确定日期区间、对时间序列数据重新采样以及对日期进行算术运算。
下面,我们设定一个自1900年1月1日开始为期42天的时间范围,具体如下。
1 Print(“Date range”, pd.date_range(‘1/1/1900’, periods=42, freq=’D’))
当然,1月肯定不足42天,因此结束日期位于2月,不信可以检查一下。
Date range DatetimeIndex(['1900-01-01', '1900-01-02', '1900-01-03', '1900-01-04', '1900-01-05', '1900-01-06', '1900-01-07', '1900-01-08', '1900-01-09', '1900-01-10',..., '1900-02-04', '1900-02-05', '1900-02-06', '1900-02-07', '1900-02-08', '1900-02-09', '1900-02-10', '1900-02-11'], dtype='datetime64[ns]', freq='D')
表 3-2 引用自 Pandas 的官方文档,描述了Pandas用的各种频率。
- B:营业日频率
- C:自定义营业日频率(试验性的)
- D:日历日频率
- W:周频率
- M:月末频率
- BM:营业月末频率
- MS:月初频率
- BMS:营业月初频率
- Q:季末频率
- BQ:营业季末频率
- QS:季初频率
- BQS:营业季初频率
- A:年终频率
- BA:营业年终频率
- AS:年初频率
- BAS:营业年初频率
- H:小时频率
- T:分钟频率
- S:秒频率
- L:毫秒
- U:微秒
在Pandas中,日期区间是有限制的。Pandas的时间戳基于NumPy datetime64类型,以纳秒(十亿分之一秒)为单位,而且用一个64位整数来表示具体数值。因此,日期有效的时间戳大体介于1677—2262年这段时间范围内。当然,这些年份中也不是所有日期都是有效的。这个时间范围的精确中点应该是1970年1月1日。这样,1677年1月1日就无法用Pandas的时间戳定义,而1677年9月30日则可以,下面我们用代码进行说明。
1 import sys 2 try: 3 print("Date range", pd.date_range('1/1/1677', periods=4, freq='D')) 4 except: 5 etype, value, _ = sys.exc_info() 6 print("Error encountered", etype, value)
以上代码会得到如下错误信息。
Error encountered <class 'pandas._libs.tslibs.np_datetime.OutOfBoundsDatetime'> Out of bounds nanosecond timestamp: 1677-01-01 00:00:00
根据前面的介绍,下面用Pandas的DateOffset函数来计算允许的日期范围,代码如下。
1 offset = pd.DateOffset(seconds=2 ** 33/10 ** 9) 2 mid = pd.to_datetime('1/1/1970') 3 print("Start valid range", mid - offset) 4 print("End valid range", mid + offset)
结果如下。
Start valid range 1969-12-31 23:59:51.410065
End valid range 1970-01-01 00:00:08.589935
我们可以用Pandas把一列字符串转换成日期数据,当然,并非所有的字符串都可以进行转换。当Pandas无法对一个字符串进行转换时,就会报错。由于不同地区对日期的定义方式会有所不同,这时就会产生歧义。下面以格式串为例进行说明,具体如下。
1 print("With format", pd.to_datetime(['19021112', '19031230'], format='%Y%m%d'))
这两个字符串可以正确地进行转换,所以不会报错。
With format DatetimeIndex(['1902-11-12', '1903-12-30'], dtype='datetime64[ns]', freq=None)
如果一个字符串明显不是日期,那么默认情况下是无法进行转换的。
1 Print(“Illegal date”, pd.to_datetime([‘1902-11-12’, ‘not a date’]))
很明显,上面的第2个字符串是无法进行转换的。
ParserError: Unknown string format: not a date
如果需要进行强制转换,必须把参数coerce设置为True。
1 print("Illegal date coerced", pd.to_datetime(['1902-11-12', 'not a date'], errors='coerce'))
显而易见,第2个字符串仍然无法转化为一个日期,所以最终得到的是一个非时间数据NaT。
Illegal date coerced DatetimeIndex(['1902-11-12', 'NaT'], dtype='datetime64[ns]', freq=None)
该例子的示例代码取自本书代码包中的ch-03.ipynb文件。
3.11数据透视表
熟悉Excel的读者都知道,数据透视表可以用来汇总数据。目前为止,本章中所见的CSV文件中的数据都是以平面文件的形式存放的。数据透视表可以从一个平面文件中指定的行和列中聚合数据,这种聚合操作可以是求和、求平均值、求标准差等运算。这里我们将再次用到前面data_aggregation.py的相关数据。因为Pandas API已经为我们提供了顶级pivot_table()函数以及相应的DataFrame方法,所以,只要设置好aggfunc参数,就可以让这个聚合函数来执行NumPy中诸如sum()之类的函数。参数cols用来告诉Pandas要对哪些列进行聚合运算。下面针对Food列来创建一个数据透视表,具体如下。
1 Print(pd.pivot_table(df, cols=[‘Food’], aggfunc=np.sum))
我们得到的数据透视表包含了完整的食物数据项。
Food chocolate icecream soup
Number 8.000000 15.000000 19.00000
3.12小结
本章主要介绍了Pandas,它是Python的数据分析程序库。我们可以将本章看成是Pandas功能特性与数据结构的基础入门资料。本章介绍了Pandas各种类似关系型数据库数据表的功能,通过这些功能,我们可以高效地进行查询、聚合及其他数据操作。通过配合使用NumPy和Pandas,我们可以完成各种基本的统计分析任务。现在,读者是不是觉得对于数据分析来说,有了Pandas就万事俱备了呢?实际上,这还远远不够。
掌握了本章介绍的基本之后,我们将在第4章中详解数据分析过程中常用的统计和数值函数。
我们鼓励读者阅读参考文献中介绍的各种书籍,以便更加详细和深入地探索Pandas。
第3章完。
随书源码官方下载:
https://www.ptpress.com.cn/shopping/buy?bookId=bae24ecb-a1a1-41c7-be7c-d913b163c111
需要登录后免费下载。