书名:pandas数据处理与分析
ISBN:978-7-115-58365-9
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 耿远昊
责任编辑 刘雅思
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
读者服务:
微信扫码关注【异步社区】微信公众号,回复“e58365”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。
本书以Python中的pandas库为主线,介绍各类数据处理与分析方法。
本书共包含13章,第一部分介绍NumPy和pandas的基本内容;第二部分介绍pandas库中的4类操作,包括索引、分组、变形和连接;第三部分介绍基于pandas库的4类数据,包括缺失数据、文本数据、分类数据和时间序列数据,并介绍这4类数据的处理方法;第四部分介绍数据观测、特征工程和性能优化的相关内容。本书以丰富的练习为特色,每章的最后一节为习题,同时每章包含许多即时性的练习(练一练)。读者可通过这些练习将对数据科学的宏观认识运用到实践中。
本书适合具有一定Python编程基础、想要使用pandas进行数据处理与分析的数据科学领域的从业者或研究人员阅读。
“pandas令人头痛!——我在学习pandas库时曾如此抱怨。
pandas库的函数令人眼花缭乱,现实中的复杂问题难免使人手足无措。如果你刚开始使用pandas,遇到报错是很正常的,即使正确地进行了修复,下次遇到类似的问题时你可能已经遗忘了先前的解决方案,这样的情况听上去令人有些沮丧。因此,我经过总结思考并结合实践,梳理了pandas中常用的函数,将本书的前3个部分划分为“1+4+4”的模块结构,即“pandas基础”+“4类pandas操作”+“4类pandas数据”,在每个模块中总结了函数之间的逻辑关系,从而展示出数据处理的宏观体系。除了数据处理,还要对数据进行分析,因此在先前的结构之上,读者还应该掌握3个问题的解决方案,即“怎么分析”“怎么处理”“怎么加速”,这对应“数据观测”“特征工程”和“性能优化”这3个知识模块。
数据处理与分析是实战型任务,读者需要通过一些高质量的练习来巩固所学知识。因此,本书配备了一定数量的习题,这些习题能够帮助读者理解、强化和拓展书中介绍的内容。
在本书写作期间,我也为pandas的1.1.0版本、1.2.0版本、1.3.0版本、1.4.0版本和1.5.0版本贡献了自己的一份力量,包括修复文档的描述性错误、修复代码中的bug以及增加函数的新特性(resample对象的逆向采样等)。虽然这些改进对整个pandas项目来说似乎微不足道,但我本人在这种开源的模式下感受到了愉悦和自我价值,因为这能体现我的分享精神、交流精神和协作精神。正所谓“一个人可以走得很快,一群人可以走得很远”,希望读者在学习过程中学会多思考、多练习、多总结,更要学会多分享、多交流、多协作,携此精神畅游数据科学的世界。
本书并不要求读者对数据科学或数据分析有先验认识,只需具备基本的Python语法知识。本书也适用于有一些pandas 基础且想要系统学习数据处理与分析方法的读者。对于已经对pandas和数据科学有一定了解的读者,阅读本书也能够起到巩固和拓展知识的作用。
本书分为基础知识(第1章、第2章)、4类操作(第3章~第6章)、4类数据(第7章~第10章)和进阶实战(第11章~第13章)4个部分。
第一部分包含Python基础、NumPy基础和pandas基础。其中,Python基础回顾推导式、匿名函数和打包函数的概念与应用;NumPy基础包含常见的数组操作,如构造、变形、切片、广播机制以及常用函数。pandas基础包含文件的读取和写入、基本数据结构、常用基本函数以及窗口对象。
第二部分介绍索引、分组、变形和连接这4类操作。其中,第3章涵盖单级索引、多级索引和常用索引方法;第4章介绍分组模式及其对象的基本概念、聚合函数的使用方法、变换函数和过滤函数的用法,以及跨列分组的相关内容;第5章讨论长宽表的变形和其他变形方法;第6章涉及关系连接的基本概念、常用关系连接函数和其他连接函数等。
第三部分介绍缺失数据、文本数据、分类数据和时间序列数据这4类数据。其中,第7章涉及缺失数据的四大操作——统计、删除、填充、插值,以及对Nullable类型的详细解读;第8章涵盖str对象、正则表达式基础、文本处理的5类操作——拆分、合并、匹配、替换、提取,以及常用字符串函数;第9章涉及cat对象、有序类别以及区间类别;第10章涵盖时间戳、时间差、日期偏置和时间序列操作的内容。
第四部分包含数据观测、特征工程和性能优化的内容。第11章介绍可视化的基本方法以及数据观测的一般思路。第12章介绍单特征构造、多特征构造和特征选择的常用方法。第13章介绍pandas代码编写的注意事项、基于多进程的加速方法、基于Cython的加速方法以及基于Numba的加速方法。
(1)练一练:读者需要结合该窗口附近的相关知识点完成窗口中列出的练习。
练一练 Ex0-1
这里是需要练习的内容。
(2)注解:读者需要注意该窗口中的说明,它们是对正文的补充或深入解读。
注解
这里是注解的内容。
(3)输入代码块:表示Jupyter单元的输入。
In [1]: # 这是输入代码块
(4)输出代码块:表示Jupyter单元的输出,每一个输出代码块必然存在一个与之对应的输入代码块。
Out[1]: # 这是输出代码块
(5)普通代码块:表示样例代码、终端输入或Python文件内容。
# 这是普通代码块
(6)代码中的省略标记用“……”表示。
本书的数据集资源和参考答案可参见GitHub网站的datawhalechina/joyful-pandas仓库。
感谢pandas社区,特别是Wes McKinney(pandas库的发明者)、Brock Mendel、Jeff Reback、Joris Van den Bossche、Tom Augspurger、Simon Hawkins、Matthew Roeschke等pandas核心开发组的成员,没有他们对开源项目的维护和坚守,就不可能有当前如此丰富的Python数据科学生态。
感谢Datawhale开源社区,本书源于Datawhale社区的“Joyful Pandas”教程。该社区为我提供了非常好的平台和机会,使我能够分享自己学习到的数据科学知识。
感谢我的母校华东师范大学。本书的大部分章节写成于我在母校就读期间,在那里我度过了难忘的学习生活。
感谢为本书付出巨大努力的人民邮电出版社的刘雅思编辑以及全体工作人员。
最后,感谢我父母多年来对我的照料和支持。
本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。
本书提供如下资源:
要获得以上配套资源,请在异步社区本书页面中点击,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。
如果您是教师,希望获得教学配套资源,请在社区本书页面中直接联系本书的责任编辑。
您还可以扫码右侧二维码, 关注【异步社区】微信公众号,回复“e58365”直接获取,同时可以获得异步社区15天VIP会员卡,近千本电子书免费畅读。
作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。
当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,点击“提交勘误”,输入勘误信息,点击“提交”按钮即可(见下图)。本书的作者和编辑会对您提交的勘误信息进行审核,确认并接受您的建议后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。
扫描下方二维码,您将会在异步社区微信服务号中看到本书信息及相关的服务提示。
我们的联系邮箱是contact@epubit.com.cn。
如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果您有兴趣出版图书、录制教学视频或者参与图书技术审校等工作,可以直接发邮件给本书的责任编辑(liuyasi@ptpress.com.cn)。
如果您来自学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接通过邮件发给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。
“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。
“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端和网络技术等。
异步社区
微信服务号
图1
图2
图3
图4
图5
图6
图7
图8
图9
图10
图11
图12
图13
图14
图15
图16
图17
图18
图19
图20
图21
图22
图23
图24
图25
图26
图27
图28
■ 第1章 预备知识
■ 第2章 pandas基础
本章中将涉及常用文件格式的输入与输出、pandas的基本数据结构、常用基本函数以及窗口对象等内容,这些内容是学习本书后续章节的基础,需要熟练掌握。我们需要检查安装的pandas版本是否不低于1.4.0。
一个包的版本号可以通过package_name._ _version_ _的方式获得。注意,version两侧均为_ _。
In [1]: import numpy as np
import pandas as pd
In [2]: pd._ _version_ _
Out[2]: '1.4.0'
注解
本书绝大多数代码都可以在pandas 1.2.0及以上版本运行。若介绍新版本的特性,会特别指出。
在数据分析的整个流程中,文件的读取和写入是不可缺少的环节,pandas提供了各类读取或写入不同格式文件的函数,这里主要介绍csv、txt和excel这3种格式文件的读取和写入。
pandas的文件输入输出模块依赖xlrd、xlwt和openpyxl这3个第三方库,若未安装可使用如下命令安装:
# 可以使用如下<em>conda</em>命令或<em>pip</em>命令安装
$ conda install xlrd xlwt openpyxl
$ pip install xlrd xlwt openpyxl
csv、txt和excel文件分别可以用read_csv()、read_table()和read_excel()读取,其中的传入参数为相应文件的绝对路径或相对路径。
In [3]: df_csv = pd.read_csv('data/ch2/my_csv.csv')
df_csv
Out[3]: col1 col2 col3 col4 col5
0 2 a 1.4 apple 2020/1/1
1 3 b 3.4 banana 2020/1/2
2 6 c 2.5 orange 2020/1/5
In [4]: df_txt = pd.read_table('data/ch2/my_table.txt')
df_txt
Out[4]: col1 col2 col3 col4 col5
0 2 a 1.4 apple 2020/1/1
1 3 b 3.4 banana 2020/1/2
2 6 c 2.5 orange 2020/1/5
In [5]: df_excel = pd.read_excel('data/ch2/my_excel.xlsx')
df_excel
Out[5]: col1 col2 col3 col4 col5
0 2 a 1.4 apple 2020/1/1
1 3 b 3.4 banana 2020/1/2
2 6 c 2.5 orange 2020/1/5
这些函数有一些公共参数,含义如下:将header设置为None表示第一行不作为列名;index_col表示把某一列或几列作为索引,索引的内容将会在第3章进行详述;usecols表示读取列的集合,默认读取所有列;parse_dates表示需要转化为时间的列,关于时间序列的内容将在第10章讲解;nrows表示读取的数据行数。上面这些参数在上述的3个函数里都可以使用。
In [6]: pd.read_table('data/ch2/my_table.txt', header=None)
Out[6]: 0 1 2 3 4
0 col1 col2 col3 col4 col5
0 2 a 1.4 apple 2020/1/1
1 3 b 3.4 banana 2020/1/2
2 6 c 2.5 orange 2020/1/5
In [7]: pd.read_csv('data/ch2/my_csv.csv', index_col=['col1', 'col2'])
Out[7]: col3 col4 col5
col1 col2
2 a 1.4 apple 2020/1/1
3 b 3.4 banana 2020/1/2
6 c 2.5 orange 2020/1/5
In [8]: pd.read_table('data/ch2/my_table.txt', usecols=['col1', 'col2'])
Out[8]: col1 col2
0 2 a
1 3 b
2 6 c
In [9]: # col5的格式已不是原先的字符串
pd.read_csv('data/ch2/my_csv.csv', parse_dates=['col5'])
Out[9]: col1 col2 col3 col4 col5
0 2 a 1.4 apple 2020/1/1
1 3 b 3.4 banana 2020/1/2
2 6 c 2.5 orange 2020/1/5
In [10]: pd.read_excel('data/ch2/my_excel.xlsx', nrows=2)
Out[10]: col1 col2 col3 col4 col5
0 2 a 1.4 apple 2020/1/1
1 3 b 3.4 banana 2020/1/2
在读取txt文件时,经常会遇到分隔符非空格的情况,read_table()有一个分割参数sep,它使得用户可以自定义分割符号来进行对txt类型数据的读取。例如,下面读取的表以“||||”为分割符号:
In [11]: pd.read_table('data/ch2/my_table_special_sep.txt')
Out[11]: col1 |||| col2
0 TS |||| This is an apple.
1 GQ |||| My name is Bob.
2 WT |||| Well done!
上面的结果显然不是我们想要的,这时可以使用参数sep,同时需要指定引擎(engine)为Python:
In [12]: pd.read_table(
'data/ch2/my_table_special_sep.txt',
sep= '\|\|\|\|',
engine= 'python'
)
Out[12]: col1 col2
0 TS This is an apple.
1 GQ My name is Bob.
2 WT Well done!
注解
在使用read_table()的时候需要注意,参数 sep 中使用的是正则表达式。在正则表达式中,“|”具有特殊含义,因此需要对“|”利用反斜杠转义为真实的“|”,否则无法读取到正确的结果。有关正则表达式的基本内容可以参考第8章或者其他资料。
一般情况下,pandas会在数据写入时包含当前的数据索引,但很多情况下我们并不需要将默认的整数索引(0~n−1,n为行数)包含到输出表中,此时我们可以把index设置为False,该操作能在保存输出表的时候把索引去除。
In [13]: df_csv.to_csv('data/ch2/my_csv_saved.csv', index=False)
df_excel.to_excel('data/ch2/my_excel_saved.xlsx', index=False)
练一练 Ex2-1
请将上面代码中的index=False删除或设置index=True,对比输出结果有何差异。
pandas中没有定义to_table()函数,但是to_csv()函数可以将数据保存为txt文件,并且允许自定义分隔符、常用制表符t分割:
In [14]: df_txt.to_csv('data/ch2/my_txt_saved.txt', sep='\t', index=False)
如果想要把表格快速转换为markdown格式和latex格式,可以使用to_markdown()函数和to_latex()函数,此处需要安装tabulate包。
pip install tabulate
In [15]: print(df_csv.to_markdown())
Out[15]: | | col1 | col2 | col3 | col4 | col5 |
|---:|-------:|:-------|-------:|:-------|:---------|
| 0 | 2 | a | 1.4 | apple | 2020/1/1 |
| 1 | 3 | b | 3.4 | banana | 2020/1/2 |
| 2 | 6 | c | 2.5 | orange | 2020/1/5 |
In [16]: print(df_csv.style.to_latex())
Out[16]: \begin{tabular}{lrlrll}
& col1 & col2 & col3 & col4 & col5 \\
0 & 2 & a & 1.400000 & apple & 2020/1/1 \\
1 & 3 & b & 3.400000 & banana & 2020/1/2 \\
2 & 6 & c & 2.500000 & orange & 2020/1/5 \\
\end{tabular}
注解
在上面的例子中,我们访问了DataFrame上的style,其返回的Styler对象能够让用户在Jupyter Notebook中对显示的表格进行自定义渲染。本书不会介绍对style对象的使用,有兴趣了解的读者可以参考pandas官方文档User Guide中对Table Visualization的详细解读。
pandas有两种基本的数据结构,分别是存储一维值属性values的Series和存储二维值属性values的DataFrame,在这两种数据结构上定义了很多属性和方法,pandas中的绝大多数数据处理操作基于它们来进行。
Series对象中包含4个重要的组成部分,分别是序列的值data、索引index、存储类型dtype和序列的名字name。其中,索引也可以指定名字。
In [17]: s = pd.Series(data = [100, 'a', {'dic1':5}],
index = pd.Index(['id1', 20, 'third'], name='my_idx'),
dtype = 'object', # 常用的dtype还有int、float、string、category
name = 'my_name')
s
Out[17]: my_idx
id1 100
20 a
third {'dic1': 5}
Name: my_name, dtype: object
注解
object代表一种混合类型,正如上面的例子中存储了整数、字符串以及Python的字典数据结构。此外,在默认状态下,pandas把纯字符串序列当作一种object类型的序列,但它也可以显式地指定string作为其类型。文本序列的内容会在第8章中讨论。
对于这些属性等内容,可以通过“.”来获取:
In [18]: s.values
Out[18]: array([100, 'a', {'dic1': 5}], dtype=object)
In [19]: s.index
Out[19]: Index(['id1', 20, 'third'], dtype='object', name='my_idx')
In [20]: s.dtype
Out[20]: dtype('O')
In [21]: s.name
Out[21]: 'my_name'
利用.shape可以获取序列的长度:
In [22]: s.shape
Out[22]: (3,)
索引是pandas中最重要的概念之一,将在第3章中详细地讨论。如果想要取出单个索引对应的值,可以通过[index_item]取出,其中index_item是索引的标签。
In [23]: s['third']
Out[23]: {'dic1': 5}
DataFrame在Series的基础上增加了列索引,可以把它理解为一种将一组具有公共索引的Series拼接而得到的数据结构。一个DataFrame可以由二维的data与行列索引来构造:
In [24]: data = [[1, 'a', 1.2], [2, 'b', 2.2], [3, 'c', 3.2]]
df = pd.DataFrame(data=data,
index=['row_%d'%i for i in range(3)], # 行索引
columns=['col_0', 'col_1', 'col_2']) # 列索引
df
Out[24]: col_0 col_1 col_2
row_0 1 a 1.2
row_1 2 b 2.2
row_2 3 c 3.2
但更多的时候会采用从列索引名到数据的映射来构造DataFrame,再加上行索引:
In [25]: df = pd.DataFrame(data = {'col_0': [1,2,3], 'col_1':list('abc'),
'col_2': [1.2, 2.2, 3.2]},
index = ['row_%d'%i for i in range(3)])
df
Out[25]: col_0 col_1 col_2
row_0 1 a 1.2
row_1 2 b 2.2
row_2 3 c 3.2
练一练 Ex2-2
在上面的df中,如果data字典的 'col_0' 键对应的不是列表,而是一个与df中索引相同的Series,会发生什么?如果它的索引和df的索引不一致,又会发生什么?
由于这种映射关系,在DataFrame中可以用[col_name]与[col_list]来取出相应的列与由多个列组成新的DataFrame,结果分别为Series和DataFrame:
In [26]: df['col_0']
Out[26]: row_0 1
row_1 2
row_2 3
Name: col_0, dtype: int64
In [27]: df[['col_0', 'col_1']]
Out[27]: col_0 col_1
row_0 1 a
row_1 2 b
row_2 3 c
练一练 Ex2-3
使用df['col_0']和df[['col_0']]得到的结果的类型有什么区别?
使用to_frame()函数可以把序列转换为列数为1的DataFrame:
In [28]: df['col_0'].to_frame()
Out[28]: col_0
row_0 1
row_1 2
row_2 3
与Series类似,在DataFrame中同样可以取出相应的属性:
In [29]: df.values
Out[29]: array([[1, 'a', 1.2],
[2, 'b', 2.2],
[3, 'c', 3.2]], dtype=object)
In [30]: df.index
Out[30]: Index(['row_0', 'row_1', 'row_2'], dtype='object')
In [31]: df.columns
Out[31]: Index(['col_0', 'col_1', 'col_2'], dtype='object')
In [32]: df.dtypes # 返回的是值为相应列数据类型的Series
Out[32]: col_0 int64
col_1 object
col_2 float64
dtype: object
In [33]: df.shape # 返回一个元组
Out[33]: (3, 3)
通过“.T”可以把DataFrame的行列进行转置:
In [34]: df.T
Out[34]: row_0 row_1 row_2
col_0 1 2 3
col_1 a b c
col_2 1.2 2.2 3.2
练一练 Ex2-4
给定一个DataFrame,请构造其转置且不得使用“.T”。
当想要对列进行修改或者新增一列时,可以直接使用df[col_name]的方式:
In [35]: df["col_0"] = df['col_0'].values[::-1] # 颠倒顺序
df["col_2"] *= 2
df["col_3"] = ["apple",banana", "cat"]
df
Out[35]: col_0 col_1 col_2 col_3
row_0 3 a 2.4 apple
row_1 2 b 4.4 banana
row_2 1 c 6.4 cat
当想要删除某一个列时,可以使用drop方法:
In [36]: df.drop(["col_3"], axis=1)
Out[36]: col_0 col_1 col_2
row_0 3 a 2.4
row_1 2 b 4.4
row_2 1 c 6.4
当axis取值为1时为删除列,而当axis取值为0时为删除行:
In [37]: df.drop(["row_1"], axis=0)
df
Out[37]: col_0 col_1 col_2 col_3
row_0 3 a 2.4 apple
row_2 1 c 6.4 cat
注解
Series或DataFrame的绝大多数方法在默认参数下都不会改变原表,而是返回一个临时拷贝。当真正需要在df上删除时,使用赋值语句df=df.drop(...)即可。
同时,利用[col_list]的方式来选出需要的列可以做到如上的等价筛选:
In [38]: df = df[df.columns[:-1]]
df
Out[38]: col_0 col_1 col_2
row_0 3 a 2.4
row_1 2 b 4.4
row_2 1 c 6.4
pandas中大量的函数是初学者学习pandas的第一只“拦路虎”,本书将它们分为3类:第一类是与索引、分组、变形和连接这4种数据操作有关的函数,它们被安排在第3章~第6章;第二类是与缺失数据、文本数据、分类数据和时间序列数据这4种数据类型有关的函数,它们被安排在第7章~第10章;剩下的基本函数被划分为第三类。本节通过功能划分将这些函数拆分到各个小节,以此来帮助读者进行模块化的学习,也便于后续的复习、查阅和整理。
为了结合具体的数据来演示函数的操作方法,我们将引入数据集learn_pandas.csv,它记录了4所学校学生的个人体测信息。本数据集中所有样本均为随机生成,与真实的个人信息无关。
In [39]: df = pd.read_csv('data/learn_pandas.csv')
df.columns
Out[39]: Index(['School', 'Grade', 'Name', 'Gender', 'Height',
'Weight', 'Transfer', 'Test_Number', 'Test_Date',
'Time_Record'], dtype='object')
上述列名依次代表学校、年级、姓名、性别、身高、体重、是否为转系生、体测场次、测试时间、1000米成绩,本节只需使用其中的前7列的数据。
In [40]: df = df[df.columns[:7]]
Series和DataFrame上存储了许多信息,但我们很多时候只需要获取其中的部分信息。当想要查看表的前几行或后几行时,可以使用head()函数和tail()函数,它们分别返回表或者序列的前n行和后n行信息,其中n默认为5:
In [41]: df.head(2)
Out[41]: School Grade Name Gender Height Weight Transfer
0 A Freshman Gaopeng Yang Female 158.9 46.0 N
1 B Freshman Changqiang You Male 166.5 70.0 N
In [42]: df.tail(3)
Out[42]: School Grade Name Gender Height Weight Transfer
197 A Senior Chengqiang Chu Female 153.9 45.0 N
198 A Senior Chengmei Shen Male 175.3 71.0 N
199 D Sophomore Chunpeng Lv Male 155.7 51.0 N
info()函数和describe()函数分别返回表的信息概况和表中数值列对应的主要统计量:
In [43]: df.info()
Out[43]: <class 'pandas.core.frame.DataFrame'>
RangeIndex: 200 entries, 0 to 199
Data columns (total 7 columns):
# Column Non-Null Count Dtype
--------- -------------------
0 School 200 non-null object
1 Grade 200 non-null object
2 Name 200 non-null object
3 Gender 200 non-null object
4 Height 183 non-null float64
5 Weight 189 non-null float64
6 Transfer 188 non-null object
dtypes: float64(2), object(5)
memory usage: 11.1+ KB
In [44]: df.describe()
Out[44]: Height Weight
count 183.000000 189.000000
mean 163.218033 55.015873
std 8.608879 12.824294
min 145.400000 34.000000
25% 157.150000 46.000000
50% 161.900000 51.000000
75% 167.500000 65.000000
max 193.900000 89.000000
注解
info() 和 describe() 只能实现对信息的初步汇总,如果想要对一个数据集进行更为全面且有效的观察,特别是在列较多的情况下,推荐使用 pandas-profiling包,我们将在 11.2.4 节中进行介绍。
在Series和DataFrame上定义了许多统计函数,最常见的是sum()、mean()、median()、var()、std()、max()和min()。下面,我们选出身高和体重列来计算它们的均值和最大值:
In [45]: df_demo = df[['Height', 'Weight']]
df_demo.mean()
Out[45]: Height 163.218033
Weight 55.015873
dtype: float64
In [46]: df_demo.max()
Out[46]: Height 193.9
Weight 89.0
dtype: float64
此外,需要介绍的是quantile()、count()和idxmax()这3个函数,它们分别返回的是分位数、非缺失值个数和最大值对应的索引:
In [47]: df_demo.quantile(0.75)
Out[47]: Height 167.5
Weight 65.0
Name: 0.75, dtype: float64
In [48]: df_demo.idxmax()# idxmin()函数结果是最小值对应的索引
Out[48]: Height 193
Weight 2
dtype: int64
由于上述所有函数对每一个序列进行操作后返回的结果是标量(单个值),因此它们又被称为聚合函数,它们有一个公共参数axis,默认值为0,代表逐列聚合,如果设置为1则表示逐行聚合:
In [49]: df_demo.mean(axis=1).head() # 在这个数据集上体重和身高的均值没有意义
Out[49]: 0 102.45
1 118.25
2 138.95
3 41.00
4 124.00
dtype: float64
练一练 Ex2-5
身体质量指数(BMI)的计算方式是体重(单位为kg)除以身高(单位为m)的平方,请找出BMI最高对应同学的姓名。
pandas中有一些函数和数据中元素出现的频次相关。对Series使用unique()和nunique()可以分别得到其唯一值组成的列表和唯一值的个数:
In [50]: df['School'].unique()
Out[50]: array(['A', 'B', 'C', 'D'], dtype=object)
In [51]: df['School'].nunique()
Out[51]: 4
通过value_counts()可以得到序列中每个值出现的次数,当设定normalize为True时会进行归一化处理。
In [52]: df['School'].value_counts()
Out[52]: D 69
A 57
C 40
B 34
Name: School, dtype: int64
In [53]: df['School'].value_counts(normalize=True)
Out[53]: D 0.345
A 0.285
C 0.200
B 0.170
Name: School, dtype: float64
如果想要观察多个列组合的唯一值,可以使用drop_duplicates()。其中的关键参数是keep,默认值first表示保留每个组合第一次出现的所在行,指定为last表示保留每个组合最后一次出现的所在行,指定为False表示把所有组合重复的所在行剔除。
In [54]: df_demo = df[['Gender','Transfer','Name']]
df_demo.drop_duplicates(['Gender', 'Transfer'])
Out[54]: Gender Transfer Name
0 Female N Gaopeng Yang
1 Male N Changqiang You
12 Female NaN Peng You
21 Male NaN Xiaopeng Shen
36 Male Y Xiaojuan Qin
43 Female Y Gaoli Feng
In [55]: df_demo.drop_duplicates(['Gender', 'Transfer'], keep='last')
Out[55]: Gender Transfer Name
147 Male NaN Juan You
150 Male Y Chengpeng You
169 Female Y Chengquan Qin
194 Female NaN Yanmei Qian
197 Female N Chengqiang Chu
199 Male N Chunpeng Lv
将keep指定为False意味着保留标签或标签组合中只出现过一次的行:
In [56]: df_demo.drop_duplicates(['Name', 'Gender'], keep=False).head()
Out[56]: Gender Transfer Name
0 Female N Gaopeng Yang
1 Male N Changqiang You
2 Male N Mei Sun
4 Male N Gaojuan You
5 Female N Xiaoli Qian
我们在Series上也可以使用drop_duplicates():
In [57]: df['School'].drop_duplicates()
Out[57]: 0 A
1 B
3 C
5 D
Name: School, dtype: object
duplicated()和drop_duplicates()的功能类似,但前者返回关于元素是否为唯一值的布尔列表,而其参数keep的意义与后者一致。duplicated()返回的序列把重复元素设为True,否则为False,drop_duplicates()等价于把duplicated()返回为True的对应行剔除。
In [58]: df_demo.duplicated(['Gender', 'Transfer']).head()
Out[58]: 0 False
1 False
2 True
3 True
4 True
dtype: bool
In [59]: df['School'].duplicated().head()
Out[59]: 0 False
1 False
2 True
3 False
4 True
Name: School, dtype: bool
一般而言,替换操作是针对某一个列进行的,因此下面的例子都使用Series。pandas中的替换可以归纳为3类:映射替换、逻辑替换和数值替换。其中,映射替换包含replace()、第8章中的str.replace()以及第9章中的cat.codes属性。此处介绍映射替换中replace()的用法。
在replace()中,可以通过字典构造或者传入两个列表(分别表示需要替换的值和替换后的值)来进行替换:
In [60]: df['Gender'].replace({'Female':0, 'Male':1}).head()
Out[60]: 0 0
1 1
2 1
3 0
4 1
Name: Gender, dtype: int64
In [61]: df['Gender'].replace(['Female', 'Male'], [0, 1]).head()
Out[61]: 0 0
1 1
2 1
3 0
4 1
Name: Gender, dtype: int64
另外,replace()还可以进行一种特殊的方向替换,指定参数method为ffill时,用前面一个最近的未被替换的值进行替换,参数method为bfill时,则用后面最近的未被替换的值进行替换。从下面的例子可以看到,它们的结果是不同的:
In [62]: s = pd.Series(['a', 1, 'b', 2, 1, 1, 'a'])
s.replace([1, 2], method='ffill')
Out[62]: 0 a
1 a
2 b
3 b
4 b
5 b
6 a
dtype: object
In [63]: s.replace([1, 2], method='bfill')
Out[63]: 0 a
1 b
2 b
3 a
4 a
5 a
6 a
dtype: object
逻辑替换包括where()和mask(),这两个函数是相对应的:where()在传入条件为False的对应行进行替换,而mask()在传入条件为True的对应行进行替换,当未对二者指定替换值时,将对应行替换为缺失值。
In [64]: s = pd.Series([-1, 1.2345, 100, -50])
s.where(s<0)
Out[64]: 0 -1.0
1 NaN
2 NaN
3 -50.0
dtype: float64
In [65]: s.where(s<0, 100)
Out[65]: 0 -1.0
1 100.0
2 100.0
3 -50.0
dtype: float64
In [66]: s.mask(s<0)
Out[66]: 0 NaN
1 1.2345
2 100.0000
3 NaN
dtype: float64
In [67]: s.mask(s<0, -50)
Out[67]: 0 -50.0000
1 1.2345
2 100.0000
3 -50.0000
dtype: float64
需要注意的是,传入的条件只需要是布尔序列即可,但其索引应当与被调用的Series索引一致:
In [68]: s_condition= pd.Series([True,False,False,True],index=s.index)
s.mask(s_condition, -50)
Out[68]: 0 -50.0000
1 1.2345
2 100.0000
3 -50.0000
dtype: float64
数值替换包含round()、abs()和clip(),它们分别表示按照给定精度四舍五入、取绝对值和截断:
In [69]: s = pd.Series([-1, 1.2345, 100, -50])
s.round(2)
Out[69]: 0 -1.00
1 1.23
2 100.00
3 -50.00
dtype: float64
In [70]: s.abs()
Out[70]: 0 1.0000
1 1.2345
2 100.0000
3 50.0000
dtype: float64
In [71]: s.clip(0, 2) # 前两个数分别表示上下截断边界
Out[71]: 0 0.0000
1 1.2345
2 2.0000
3 0.0000
dtype: float64
练一练 Ex2-6
在clip()中,超过边界的值只能被截断为边界值,如果要把超出边界的值替换为自定义的值,可以如何操作?
pandas中有两种排序函数,第一种是值排序函数sort_values(),第二种是索引排序函数sort_ index()。为了演示排序函数的使用方法,下面先利用 set_index()把年级和姓名两列作为索引,多级索引的内容和索引设置的方法将在第 3 章进行详细讲解。
In [72]: df_demo = df[['Grade', 'Name', 'Height', 'Weight']].set_index(['Grade','Name'])
默认参数ascending为True表示升序,对身高进行排序:
In [73]: df_demo.sort_values('Height').head()
Out[73]: Height Weight
Grade Name
Junior Xiaoli Chu 145.4 34.0
Senior Gaomei Lv 147.3 34.0
Sophomore Peng Han 147.8 34.0
Senior Changli Lv 148.7 41.0
Sophomore Changjuan You 150.5 40.0
当ascending为False时表示对身高进行降序排列:
In [74]: df_demo.sort_values('Height', ascending=False).head()
Out[74]: Height Weight
Grade Name
Senior Xiaoqiang Qin 193.9 79.0
Mei Sun 188.9 89.0
Gaoli Zhao 186.5 83.0
Freshman Qiang Han 185.3 87.0
Senior Qiang Zheng 183.9 87.0
在排序中,经常会遇到多列排序的问题,例如在体重相同的情况下,对身高进行排序,并且保持身高降序排列,体重升序排列。对应代码如下:
In [75]: df_demo.sort_values(['Weight','Height'],ascending=[True,False]).head()
Out[75]: Height Weight
Grade Name
Sophomore Peng Han 147.8 34.0
Senior Gaomei Lv 147.3 34.0
Junior Xiaoli Chu 145.4 34.0
Sophomore Qiang Zhou 150.5 36.0
Freshman Yanqiang Xu 152.4 38.0
索引排序的用法和值排序几乎完全一致,只不过元素的值在索引中,此时需要指定索引层的名字或者层号,用参数level表示。
In [76]: # 对年级按升序排列,对名字按降序排列
df_demo.sort_index(level=['Grade','Name'],ascending=[True,False]).head()
Out[76]: Height Weight
Grade Name
Freshman Yanquan Wang 163.5 55.0
Yanqiang Xu 152.4 38.0
Yanqiang Feng 162.3 51.0
Yanpeng Lv NaN 65.0
Yanli Zhang 165.1 52.0
注解
字符串序列的排列顺列由字母顺序决定。
此外,pandas的rank()函数也与元素的排序相关,它返回每个元素在整个序列中的排名,参数ascending表示升序排序,pct表示是否返回元素对应的分位数。
In [77]: s = pd.Series(list("ebcad"))
s.rank(ascending=True, pct=False)
Out[77]: 0 5.0
1 2.0
2 3.0
3 1.0
4 4.0
dtype: float64
参数method用于控制元素相等时的排名处理方式,默认为“average”,取均值。取“min”和“max”时分别表示取最小的可能排名和最大的可能排名,取“first”时表示按照序列中元素出现的先后顺序排名,取“dense”时表示相邻大小的元素排名相差1。从下面的例子中可以看出它们的区别:
In [78]: s = pd.Series(list("abac"))
df_rank = pd.DataFrame()
for method in ["average", "min", "max", "first", "dense"]:
df_rank[method] = s.rank(method=method)
df_rank
Out[78]: average min max first dense
0 1.5 1.0 2.0 1.0 1.0
0 1.5 1.0 2.0 1.0 1.0
1 3.0 3.0 3.0 3.0 2.0
2 1.5 1.0 2.0 2.0 1.0
3 4.0 4.0 4.0 4.0 3.0
apply()函数常用于对DataFrame进行行迭代或者列迭代,它的axis的含义与2.3.2节中的统计聚合函数的axis的含义一致。apply()的参数往往是一个以序列为输入的函数,例如,对于mean(),使用apply()可以写出:
In [79]: df_demo = df[['Height', 'Weight']]
def my_mean(x):
res = x.mean()
return res
df_demo.apply(my_mean)
Out[79]: Height 163.218033
Weight 55.015873
dtype: float64
对于简单的函数,可以利用Lambda表达式使得书写简洁,如下代码中的x就指代被调用的df_demo表中逐个输入的序列:
In [80]: df_demo.apply(lambda x:x.mean())
Out[80]: Height 163.218033
Weight 55.015873
type: float64
若指定axis=1,那么每次传入函数的就是由行元素组成的Series,其结果与2.3.2节例子中的逐行均值的结果一致。
In [81]: df_demo.apply(lambda x:x.mean(), axis=1).head()
Out[81]: 0 102.45
1 118.25
2 138.95
3 41.00
4 124.00
dtype: float64
这里再举一个例子:mad()返回的是一个序列中元素偏离该序列均值的绝对值大小的均值,例如序列[1,3,7,10]中,均值为5.25,每一个元素偏离的绝对值为[4.25,2.25,1.75,4.75],这个偏离序列的均值为3.25。现在利用apply()、mad()计算身高和体重的指标:
In [82]: df_demo.apply(lambda x:(x-x.mean()).abs().mean())
Out[82]: Height 6.707229
Weight 10.391870
dtype: float64
In [83]: df_demo.mad()
Out[83]: Height 6.707229
Weight 10.391870
dtype: float64
既然apply()如此强大,是不是就意味着其他的函数都没有用武之地,它们都可以用自定义函数来替换呢?答案是否定的,apply()的自由性是牺牲性能换来的。当apply()中迭代的行或列的数量较多时,运算时间明显变长。仍然以计算身高和体重的均值这一过程为例,我们来分别比较在200行数据样本下使用apply()和使用内置函数的性能差异:
In [84]: %timeit -n 100 -r 7 df_demo.apply(lambda x:x.mean(), axis=1)
Out[84]: 16.6ms ± 347 µs per loop (mean ± std.dev.of 7 runs, 100 loops each)
In [85]: %timeit df_demo.mean(1)
Out[85]: 182µs ± 10.7 µs per loop (mean ± std.dev.of 7 runs, 100 loops each)
注解
在Jupyter中可以使用%timeit来估计一行代码所运行的时间,其中参数-r表示运行的轮数(runs),参数-n表示每一轮该代码运行的次数(loops)。
从结果可以看到,在这个例子中它们竟存在高达约100倍的时长差距,这样的结果显然是我们不愿看见的。从另一方面说,我们应该在何时使用apply()?笔者认为只有当不存在内置函数能够解决当下的计算问题且apply()的迭代次数较少时,我们才考虑使用apply()来辅助完成计算任务。总之,在apply()的“诱惑”前,我们应当保持谨慎。
pandas中有3类窗口,分别是滑动窗口rolling、扩张窗口expanding以及指数加权窗口ewm,窗口能够在序列上通过窗口函数进行聚合。以日期偏置为窗口大小的滑动窗口将在第10章讨论,指数加权窗口见本章习题。
要使用滑动窗口(简称滑窗)函数,就必须要对一个序列使用“.rolling”以得到滑窗对象,其最重要的参数为窗口大小window。
In [86]: s = pd.Series([1,2,3,4,5])
roller = s.rolling(window = 3)
roller
Out[86]: Rolling [window=3,center=False,axis=0,method=single]
我们可以对一个窗口中的元素进行聚合,图2.1展示了滑窗均值的计算过程。
图2.1 滑窗均值的计算过程
调用滑窗的mean()方法即可得到相应结果:
In [87]: roller.mean() # NaN表示缺失值,说明窗口元素未满,无法计算
Out[87]: 0 NaN
1 NaN
2 2.0
3 3.0
4 4.0
dtype: float64
注解
除window之外,min_periods也是rolling()的一个常用参数,它指定了参与计算的最小样本量,默认为窗口大小。例如,上文中的第一个和第二个元素对应的窗口内样本数量均小于窗口大小,则直接取缺失值。当令min_periods为1时,输出的第一个和第二个元素为窗口内元素的均值。
2.3.2节中介绍的特征统计函数都能够用于滑窗:
In [88]: roller.sum()
Out[88]: 0 NaN
1 NaN
2 6.0
3 9.0
4 12.0
dtype: float64
In [89]: roller.max()
Out[89]: 0 NaN
1 NaN
2 3.0
3 4.0
4 5.0
dtype: float64
对于滑动协方差或滑动相关系数的计算如下所示:
In [90]: s2 = pd.Series([1,2,6,16,30])
roller.cov(s2)
Out[90]: 0 NaN
1 NaN
2 2.5
3 7.0
4 12.0
dtype: float64
In [91]: roller.corr(s2)
Out[91]: 0 NaN
1 NaN
2 0.944911
3 0.970725
4 0.995402
dtype: float64
此外,滑窗还支持使用apply()传入自定义函数,其传入参数是对应窗口的Series,例如上述的均值函数可以等效表示为:
In [92]: roller.apply(lambda x:x.mean())
Out[92]: 0 NaN
1 NaN
2 2.0
3 3.0
4 4.0
dtype: float64
shift()、diff()和pct_change()是一组类滑窗函数,它们的公共参数为periods=n,默认值为1,它们分别表示取向前第n个元素的值、与向前第n个元素作差和与向前第n个元素相比而计算出的增长率。这里的n可以为负值,表示反方向进行类似操作。
In [93]: s = pd.Series([1,3,6,10,15])
s.shift(2)
Out[93]: 0 NaN
1 NaN
2 1.0
3 3.0
4 6.0
dtype: float64
In [94]: s.diff(3)
Out[94]: 0 NaN
1 NaN
2 NaN
3 9.0
4 12.0
dtype: float64
In [95]: s.pct_change()
Out[95]: 0 NaN
1 2.000000
2 1.000000
3 0.666667
4 0.500000
dtype: float64
In [96]: s.shift(-1)
Out[96]: 0 3.0
1 6.0
2 10.0
3 15.0
4 NaN
dtype: float64
In [97]: s.diff(-2)
Out[97]: 0 -5.0
1 -7.0
2 -9.0
3 NaN
4 NaN
dtype: float64
练一练 Ex2-7
NumPy中也有一个同名函数np.diff(),它与pandas中diff的功能相同吗?请查阅文档并说明。
将其视作类滑窗函数的原因是,它们的功能可以用窗口大小为n+1的滑窗函数等价代替:
In [98]: s.rolling(3).apply(lambda x:list(x)[0]) # s.shift(2)
Out[98]: 0 NaN
1 NaN
2 1.0
3 3.0
4 6.0
dtype: float64
In [99]: s.rolling(4).apply(lambda x:list(x)[-1]-list(x)[0]) # s.diff(3)
Out[99]: 0 NaN
1 NaN
2 NaN
3 9.0
4 12.0
dtype: float64
In [100]: def my_pct(x):
L = list(x)
return L[-1]/L[0]-1
s.rolling(2).apply(my_pct) # s.pct_change()
Out[100]: 0 NaN
1 2.000000
2 1.000000
3 0.666667
4 0.500000
dtype: float64
练一练 Ex2-8
rolling对象的默认窗口都是向下滑动的,在某些情况下用户需要逆向滑动的窗口,例如对[1,2,3]设定窗口为2的逆向sum操作,结果为[3,5,NaN],此时应该如何实现?
扩张窗口又称累计窗口,它本质上是一个动态长度窗口,其窗口的长度就是从序列开始处的位置到具体操作处的位置,其使用的聚合函数会作用于这些逐步扩张的窗口。具体地说,设序列为[a1,a2,a3,a4],则其每个位置对应的窗口即[a1]、[a1,a2]、[a1,a2,a3]、[a1,a2,a3,a4]。
In [101]: s = pd.Series([1, 3, 6, 10])
# 可选max()、min()、median()、std()、var等
# 参见pandas官方API文档中的Expanding window functions
s.expanding().mean()
Out[101]: 0 1.000000
1 2.000000
2 3.333333
3 5.000000
dtype: float64
在NumPy中,cumsum()和cumprod()分别计算当前位置与之前所有元素的总和与乘积:
In [102]: s.values.cumsum()
Out[102]: array([ 1, 4, 10, 20], dtype=int64)
In [103]: s.values.cumprod()
Out[103]: array([ 1, 3, 18, 180], dtype=int64)
上述操作就是一种扩张窗口操作,在pandas中可以类似地写出:
In [104]: s.cumsum()
Out[104]: 0 1
1 4
2 10
3 20
dtype: int64
In [105]: s.cumprod()
Out[105]: 0 1
1 3
2 18
3 180
dtype: int64
与rolling对象的apply()方法类似,我们可以利用expanding对象上的apply()方法来等价地完成上述操作:
In [106]: s.expanding().apply(lambda x: x.sum())
Out[106]: 0 1.0
1 4.0
2 10.0
3 20.0
dtype: float64
In [107]: s.expanding().apply(lambda x: x.prod())
Out[107]: 0 1.0
1 3.0
2 18.0
3 180.0
dtype: float64
data/ch2/clothing_store.csv中记录了某服装店的商品信息,每件商品都有一级类别(type_1)、二级类别(type_2)、进价(buy_price)、售价(sale_price)和唯一的商品编号(product_id)。请注意,在本题中仅允许使用本章中介绍的函数,不得使用后续章节介绍的功能或函数(例如loc和groupby),但读者可在学习完后续章节后,自行给出基于其他方案的解答。
● 利润指售价与进价之差,求商品的平均利润。
● 从原表构造一个同长度的Series,索引是商品编号,value中的每个元素是对应位置的商品信息字符串,字符串格式为“商品一级类别为×××,二级类别为×××,进价和售价分别为×××和×××。”
● 表中有一个商品的二级类别与一级类别明显无法对应,例如一级类别为上衣,但二级类别是拖鞋,请找出这个商品对应的商品编号。
● 求各二级类别中利润最高的商品编号。
data/ch2/student_grade.csv中记录了某课程中每位学生的学习情况,包含学生编号、期中考试分数、期末考试分数、回答问题次数和缺勤次数。请注意,本题同样只能使用本章介绍的函数。
● 求出在缺勤次数最少的学生中回答问题次数最多的学生编号。
● 按如下规则计算每位学生的总评分数:(1)总评分数为百分之四十的期中考试成绩加百分之六十的期末考试成绩;(2)每回答一次问题,学生的总评分数加1分,但加分的总次数不得超过10次;(3)每缺勤一次,学生的总评分数扣5分;(4)当学生缺勤次数高于5次时,总评分数直接按0分计算;(5)总评最高分数为100分,最低分数为0分。
● 在表中新增一列“等第”,规定当学生总评分数低于60分时等第为不及格,总评分数不低于60分且低于80分时为及格,总评分数不低于80分且低于90分时为良好,总评分数不低于90分时为优秀,请统计各个等第的学生比例。
(1)作为扩张窗口的ewm窗口。
在扩张窗口中,用户可以使用各类函数进行历史的累计指标统计,但这些内置的统计函数往往给窗口中的所有元素赋予了同样的权重。事实上,可以给出不同的权重来赋予窗口中的元素,指数加权窗口就是这样一种特殊的扩张窗口。
其中,最重要的参数是α,它决定了默认情况下的窗口权重为wi = (1−α)t−i,i∈{0,1,…,t},其中w0 表示序列第一个元素x0的权重,wt表示当前元素xt的权重。从权重公式可以看出,离开当前值越远则权重越小,若记原序列为x,更新后的当前元素为yt,此时通过加权公式归一化后可知:
对Series而言,可以用ewm对象通过如下计算得到指数平滑后的序列:
In [108]: np.random.seed(0)
s = pd.Series(np.random.randint(-1,2,30).cumsum())
s.head()
Out[108]: 0 -1
1 -1
2 -2
3 -2
4 -2
dtype: int32
In [109]: s.ewm(alpha=0.2).mean().head()
Out[109]: 0 -1.000000
1 -1.000000
2 -1.409836
3 -1.609756
4 -1.725845
dtype: float64
请用expanding实现上述功能。
(2)作为滑动窗口的ewm窗口。
从第(1)问中可以看到,ewm窗口作为一种扩张窗口的特例,只能从序列的第一个元素开始加权。现在希望给定一个限制窗口n,只将包含自身的最近的n个元素作为窗口进行滑动加权平滑。请根据滑窗函数,给出新的wi与yt的公式,并通过rolling实现这一功能。
微信扫码关注【异步社区】微信公众号,回复“e58365”获取本书配套资源以及异步社区15天VIP会员卡,近千本电子书免费畅读。