书名:Python数据分析(第2版)
ISBN:978-7-115-48117-7
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 [美] 阿曼多•凡丹戈(Armando Fandango)
译 韩 波
责任编辑 胡俊英
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
Copyright ©2017 Packt Publishing. First published in the English language under the title Python Data Analysis,Second Edition.
All rights reserved.
本书由英国Packt Publishing公司授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。
版权所有,侵权必究。
Python作为一种高级程序设计语言,凭借其简洁、易读及可扩展性日渐成为程序设计领域备受推崇的语言。同时,Python语言的数据分析功能也逐渐为大众所认可。
本书就是一本介绍如何用Python进行数据分析的学习指南。全书共12章,从Python程序库入门、NumPy数组和Pandas入门开始,陆续介绍了数据的检索、数据加工与存储、数据可视化等内容。同时,本书还介绍了信号处理与时间序列、应用数据库、分析文本数据与社交媒体、预测性分析与机器学习、Python生态系统的外部环境和云计算、性能优化及分析、并发性等内容。在本书的最后,还采用3个附录的形式为读者补充了一些重要概念、常用函数以及在线资源等重要内容。
本书延续了上一版示例丰富、简单易懂的优点,非常适合对Python语言感兴趣或者想要使用Python语言进行数据分析的读者参考阅读。
Armando Fandango是Epic工程咨询集团首席数据科学家,负责与国防和政府机构有关的保密项目。Armando是一位技术精湛的技术人员,拥有全球创业公司和大型公司的工作经历和高级管理经验。他的工作涉及金融科技、证券交易所、银行、生物信息学、基因组学、广告技术、基础设施、交通运输、能源、人力资源和娱乐等多个领域。
Armando在预测分析、数据科学、机器学习、大数据、产品工程、高性能计算和云基础设施等项目中工作了十多年。他的研究兴趣横跨机器学习、深度学习和科学计算等领域。
我要特别感谢我的妻子在编写本书过程中给予我的支持。同时,我还要感谢UCF的Paul Wiegand博士,他总是鼓励我寻求机会。最后,我非常感谢Packt团队的Tushar、Sumeet、Amrita、Deepti以及其他工作人员,感谢他们为本书的面世所付出的巨大努力。
Joran Beasley毕业于爱达荷大学,获得了计算机科学学士学位。7年以来,他一直从事基于Python的桌面应用程序编程,同时将其应用于监控农业的大规模传感器网络。目前,他居住于爱达荷州莫斯科市,作为软件工程师供职于METER集团。
我要感谢我的妻子Nicole,当我把大量的时间倾注在写作上的时候,她在默默地抚养我们的两个孩子。
Ratan Kumar 在过去的几年里一直在利用各种语言和技术从事软件编程。2013 年以来,Ratan Kumar在Web服务领域使用Python语言,他发现无论是在个人项目还是专业项目方面,Python都是最优雅、最高效、最易于接受的编程语言之一。Ratan目前住在班加罗尔,是一个旨在简化股票市场投资的小型团队的核心成员。
数据分析在自然科学、生物医学和社会科学领域有着悠久的历史。随着数据科学的发展,数据分析也呈现流行之势,几乎已经渗透到工业的方方面面。与数据科学类似,数据分析也致力于从数据中提取有效信息。为此,我们需要用到统计学、机器学习、信号处理、自然语言处理和计算机科学领域中的各种技术。
在第1章中,我们将给出一幅描绘与数据分析相关的Python软件的脑图。首先要知道的是,Python生态系统已经非常完备,具有诸如NumPy、SciPy和Matplotlib等著名的程序包。当然,这没有什么好奇怪的,因为Python在1989年就诞生了。Python易学、易用,而且与其他程序设计语言相比语法简练,可读性非常强,即使从未接触过Python的人,也可以在几天内掌握该语言的基本用法,对熟悉其他编程语言的人来说尤其如此。你无需太多的基础知识,就能顺畅地阅读本书。此外,关于Python的书籍、课程和在线教程也非常多。
第1章“Python程序库入门”手把手地指导读者正确安装配置Python和基础的Python数值分析软件库。同时,本章还会展示如何通过NumPy创建一个小程序以及如何利用Matplotlib来绘制简单的图形。
第2章“NumPy数组”介绍NumPy和数组的基础知识。通过阅读本章,读者能够基本掌握NumPy数组及其相关函数。
第3章“Pandas入门”阐述Pandas的基本功能,其中涉及Pandas的数据结构与相应的操作。
第4章“统计学与线性代数”对线性代数和统计函数做了简要回顾。
第5章“数据的检索、加工与存储”介绍如何获取不同格式的数据,以及原始数据的清洗和存储方法。
第6章“数据可视化”介绍如何利用Matplotlib和Pandas的绘图函数来实现数据的可视化。
第7章“信号处理与时间序列”利用太阳黑子周期数据来实例讲解时间序列和信号处理,同时还会介绍一些相关的统计模型。本章使用的主要工具是NumPy和SciPy。
第8章“应用数据库”介绍各种数据库和有关API的知识,其中包括关系数据库和NoSQL数据库。
第9章“分析文本数据和社交媒体”考察基于文本数据的情感分析和主题抽取。同时,本章还将为读者展示一个网络分析方面的实例。
第10章“预测性分析与机器学习”通过一个例子来说明人工智能在天气预报上的应用,这主要借助于scikit-learn。不过,有些机器学习算法在scikit-learn中尚未实现,所以有时还要求助其他API。
第11章“Python生态系统的外部环境和云计算”将提供各种实例,来说明如何集成非Python编写的现有代码。此外,本章还将为读者演示如何在云中使用Python。
第12章“性能优化、性能分析与并发性”为读者介绍通过性能分析(Profling)和Cython等关键技术来改善性能的各种技巧,同时还为读者介绍多核和分布式系统方面的相关框架。
附录A“重要概念”将对本书中涉及的重要概念进行简要介绍。
附录B“常用函数”概述本书中用到的程序库中的各种函数,以便于读者查阅。
本书中的示例代码可以在大部分现代操作系统上运行;所有章节中的代码都需要用到3.5.0版本以上的Python和pip3软件。Python 3.5.x的下载地址为https://www.python.org/ downloads/,这个页面上不仅提供了用于Windows和Mac OS X的安装程序,也提供了用于Linux、UNIX和Mac OS X系统的Python源代码。多数情况下,我们都得通过运行以下命令来以管理员权限安装各种本书要用到的Python库:
$ pip3 install <some library>
以下是用于本书中的示例的Python库的清单。
除了各种Python库之外,我们还需要以下软件。
通常情况下,对于以上的程序化和软件,我们应该选用最新版本。
以上列出的某些软件只是用于某个示例,因此在安装之前,请先检查该软件是否仅限用于某个示例代码。
对于通过pip安装的Python程序包,卸载方法如下所示:
$ pip3 uninstall <some library>
本书的目标读者是对Python和数学有基本了解,并且想进一步学习如何利用Python软件进行数据分析的朋友。我们力争让本书简单易懂,但无法保证所有主题都面面俱到。如果需要,读者可以经由Khan Academy、Coursera之类的在线资源来复习自己的数学知识。
下列Packt出版社的书籍是推荐给读者的进阶读物。
本书中,不同类型的信息会采用不同的排版样式,以示区别。下面针对各种排版样式及其含义进行举例说明。
文本、数据库表名、文件夹名、文件名、文件扩展名和路径名、伪URL、用户输入和推特句柄(Twitter handles)中出现的代码文字,会显示:“如果您的当前登录账户没有足够权限的话,则需要在这条命令前面加上sudo。”
代码段显示格式如下。
def pythonsum(n):
a = list(range(n))
b = list(range(n))
c = []
for i in range(len(a)):
a[i] = i ** 2
b[i] = i ** 3
c.append(a[i] + b[i])
return c
所有的命令行输入或者输出内容会显示为如下格式。
$ pip3 install numpy scipy pandas matplotlib jupyter notebook
警告或者重要的提示在此显示。
小技巧在此显示。
本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。
本书提供如下资源:
本书源代码;
书中彩图文件。
要获得以上配套资源,请在异步社区本书页面中点击,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。
如果您是教师,希望获得教学配套资源,请在社区本书页面中直接联系本书的责任编辑。
作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。
当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,点击“提交勘误”,输入勘误信息,单击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。
我们的联系邮箱是contact@epubit.com.cn。
如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/submission即可)。
如果您是学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。
“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。
“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。
异步社区
微信服务号
欢迎来到Python数据分析的世界!如今,Python已成为数据分析和数据科学事实上的标准语言和标准平台之一。我们将为读者展示一张思维导图,图1-1中将给出Python生态系统为数据分析师和数据科学家提供的各种程序库。NumPy、SciPy、Pandas和Matplotlib库共同构成了Python数据分析的基础,当前它们已经成为SciPy Stack 1.0的组成部分。在本章中,我们不仅会学习如何安装SciPy Stack 1.0和Jupyter Notebook,还将编写一些简单的数据分析代码,为后面的学习做好热身。
下面是Python生态系统为数据分析师和数据科学家提供的常用程序库。
对于本书而言,当需要安装软件时,我们会在恰当的时机给出相应的安装说明。在安装软件的过程中遇到困难或者不能断定最佳方案时,读者可以参考图1-1,这里提供了寻找解决问题所需辅助信息的指南。
图1-1
本章将涉及以下主题。
本书所用软件都是基于Python 3的,所以必须首先安装Python 3。不过,对于某些操作系统而言,Python 3是默认安装的。Python具有多种实现,其中包括具有商业版权的实现和发行版。在本书中,我们只关注标准Python实现,因为它与NumPy完全兼容。
提示
读者可以从https://www.python.org/download/页面下载Python 3.5.x。在这个网站上,我们可以找到为Windows和Mac OS X系统开发的安装程序,以及为Linux、UNIX和Mac OS X系统提供的源码包。我们可以从https://docs.python.org/3/using/index.html上找到在各种操作系统上安装和使用Python的相关说明。
本章需要安装的软件,在Windows、各种Linux发行版本和Mac OS X系统上都有相应的二进制安装程序。当然,如果读者愿意,也可以使用相应的源代码发行包。对于Python,要求其版本为3.5.x或更高。Python 2.7版本的支持与维护工作已经从2015年延续至2020年,之后,我们不得不迁移到Python 3。
下面开始介绍如何在Windows、Linux和Mac OS X上安装和设置NumPy、SciPy、Pandas、Matplotlib、IPython和Jupyter Notebook。下面来详细了解一下这个过程。在本书中,我们将使用pip3来安装这些库。因为从3.4版本起,pip3已经默认包含在Python的安装程序中了。
为了安装这些基础的程序库,可以运行以下命令。
$ pip3 install numpy scipy pandas matplotlib jupyter notebook
如果当前登录的账户没有足够权限,则需要在上面的命令行前面添加sudo。
在撰写本书时,我们在Windows 10虚拟机上安装了以下软件,作为安装这些程序库的先决条件。
下载并安装适用于Windows平台的、预编译好的NumPy和SciPy二进制文件。
安装上述软件后,为了安装其余的基础程序库,可以运行以下命令。
$ pip3 install pandas matplotlib jupyter
小技巧
使用这些命令安装Jupyter时,要先安装所有必需的软件包,如Notebook和IPython。
我们知道,科学家、数据分析师和工程师经常需要进行实验,而IPython正是为实验而生的。对于IPython提供的交互式环境,明眼人一看就知道它与MATLAB、Mathematica和Maple非常接近。
下面是IPython shell的一些特性。
下面给出IPython shell的使用方法。
$ ipython3
Python 3.5.2 (default, Sep 28 2016, 18:08:09)
Type "copyright", "credits" or "license" for more information.
IPython 5.1.0 -- An enhanced Interactive Python.
? -> Introduction and overview of IPython's features.
%quickref -> Quick reference.
Help -> Python's own help system.
object? -> Details about 'object', use 'object??' for extra
details.
In [1]: quit()
小技巧
退出IPython shell时,可以使用quit()函数或者Ctrl+D组合键。
In [1]: %logstart
Activating auto-logging. Current session state plus future
input saved:
Filename : ipython_log.py
Mode : rotate
Output logging : False
Raw input log : False
Timestamping : False State : active
使用下列命令可以关闭记录功能。
In [9]: %logoff
Switching logging OFF
In [1]: !date
事实上,任何前置了“!”号的命令行都将发送给系统的shell来处理。此外,可以通过如下方法来存储命令的输出结果。
In [2]: thedate = !date
In [3]: thedate
In [1]: a = 2 + 2
In [2]: a
Out[2]: 4
In [3]: %hist
a = 2 + 2
a
%hist
这在命令行接口(Command Line Interface,CLI)环境中是一种非常普遍的功能。此外,我们还可以用-g开关在历史命令中进行搜索,例子如下。
In [5]: %hist -g a = 2
1: a = 2 + 2
在上面的过程中,我们使用了一些所谓的魔力函数(magic functions),这些函数均以“%”开头。当魔力函数单独用于一行时,就可以省略前缀“%”。
当使用IPython导入相应的程序库后,可以通过help命令打开NumPy函数的手册页——即使不知道该函数的确切名称。我们可以先输入几个字符,然后利用Tab键就可以自动补全剩下的字符。下面以arange()函数为例,说明如何查阅与其有关的资料。
这里给出两种翻阅相关信息的方法。
调用help函数:输入help命令(并输入函数名中的前几个字符,再按Tab键。这时将出现一个函数列表(见图1-2),我们可以通过方向键从函数名列表中进行选择,然后按Enter键进行确认),最后按Enter键盘,结束help函数的调用。
通过问号进行查询:另一种方法是在函数名后面加上问号,当然,前提条件是我们已经知道函数名,好处是不必输入help命令,例子如下。
图1-2
In [3]: numpy.arange?
Tab补全功能依赖于readline,所以务必确保先前已经安装了该软件。如果没有安装,可以使用pip完成安装,具体命令如下。
$ pip3 install readline
利用问号,我们可以从文档字符串(docstrings)中获取所需信息。
Jupyter Notebook以前被称为IPython Notebooks,它提供了一种以特殊格式创建和共享具有文本、图表和Python代码的网页的工具。
很多时候,notebook都是用于演示Python软件,或者用作一款教学工具。我们可以单纯使用Python代码或者通过特殊的notebook格式来导入和导出notebook。另外,notebook既可以在本机上跑,也可以放到专用的notebook服务器上在线使用。某些云计算解决方案(如Wakari和PiCloud)还支持在云中运行notebook。云计算的主题将在第11章中加以介绍。
为了使用Jupyter Notebook启动一个会话,读者可以使用如下命令。
$ jupyter-notebook
这时将启动notebook服务器并打开一个网页,显示该命令所在文件夹的内容。然后,你可以在Python 3中选择New | Python 3菜单项来启动一个新的notebook。
你也可以打开本书的代码包中提供的ch-01.ipynb。ch-01是一个notebook文件,其中存放了本章中简单应用程序的代码。
安装好NumPy后,就可以来看NumPy数组了。与Python中的列表相比,进行数值运算时NumPy数组的效率要高得多。事实上,NumPy数组是针对某些对象进行了大量的优化工作。
完成相同的运算时,与Python代码相比,NumPy代码用到的显式循环语句明显要少,因为NumPy是基于向量化的运算。还记得高等数学中标量和向量的概念吗?例如,数字2是一个标量,计算2+2时,进行的是标量加法运算。通过一组标量,我们可以构建出一个向量。用Python编程的术语来说,我们得到了一个一维数组。当然,这个概念可以扩展至更高的维度。实际上,针对两个数组的诸如加法之类的运算,可以将其转化为一组标量运算。使用纯Python时,为了完成该操作,可以使用循环语句遍历第一个数组中的每个元素,并与第二个数组中对应的元素相加。然而,在数学家眼里,这种方法过于繁琐。数学上,可以将这两个向量的加法视为单一操作。实际上,NumPy数组也可以这么做,而且它用低级C例程针对某些操作进行了优化处理,使得这些基本运算效率大为提高。关于NumPy数组将在第2章中详细介绍。
假设要对向量a和b进行求和。注意,这里“向量”的含义是数学意义上的,即一个一维数组。在第4章中,将遇到一种表示矩阵的特殊NumPy数组。向量a存放的是整数0~n−1的2次幂。如果n等于3,那么a保存的是0、1或4。向量b存放的是整数0~n的3次幂,所以如果n等于3,那么向量b等于0、1或者8。如果使用普通的Python代码,该怎么做呢?
在我们想出了一个解决方案后,可以拿来与等价的NumPy方案进行比较。
下面的函数没有借助NumPy,而是使用纯Python来解决向量加法问题。
def pythonsum(n):
a = list(range(n))
b = list(range(n))
c = []
for i in range(len(a)):
a[i] = i ** 2
b[i] = i ** 3
c.append(a[i] + b[i])
return c
下面是利用NumPy解决向量加法问题的函数。
def numpysum(n):
a = numpy.arange(n) ** 2
b = numpy.arange(n) ** 3
c = a + b
return c
注意,numpysum()无需使用for语句。此外,我们使用了来自NumPy的arange()函数,它替我们创建了一个含有整数0~n的NumPy数组。这里的arange()函数也是从NumPy导入的,所以它加上了前缀numpy。
现在到了真正有趣的地方。我们在前面讲过,NumPy在进行数组运算时,速度是相当快的。可是到底有多快呢?下面的程序代码将为我们展示numpysum()和pythonsum()这两个函数的实耗时间,这里以μs(微秒)为单位。同时,它还会显示向量sum最后面的两个元素值。下面来看使用Python和NumPy能否得到相同的答案。
#!/usr/bin/env/python
import sys
from datetime import datetime
import numpy as np
"""
This program demonstrates vector addition the Python way.
Run the following from the command line:
python vectorsum.py n
Here, n is an integer that specifies the size of the vectors.
The first vector to be added contains the squares of 0 up to n.
The second vector contains the cubes of 0 up to n.
The program prints the last 2 elements of the sum and the elapsed time:
"""
def numpysum(n):
a = np.arange(n) ** 2
b = np.arange(n) ** 3
c = a + b
return c
def pythonsum(n):
a = list(range(n))
b = list(range(n))
c = []
for i in range(len(a)):
a[i] = i ** 2
b[i] = i ** 3
c.append(a[i] + b[i])
return c
size = int(sys.argv[1])
start = datetime.now()
c = pythonsum(size)
delta = datetime.now() - start
print("The last 2 elements of the sum", c[-2:])
print("PythonSum elapsed time in microseconds", delta.microseconds)
start = datetime.now()
c = numpysum(size)
delta = datetime.now() - start
print("The last 2 elements of the sum", c[-2:])
print("NumPySum elapsed time in microseconds", delta.microseconds)
对于1000个、2000个和4000个向量元素,程序的结果如下。
$ python3 vectorsum.py 1000
The last 2 elements of the sum [995007996, 998001000]
PythonSum elapsed time in microseconds 976
The last 2 elements of the sum [995007996 998001000]
NumPySum elapsed time in microseconds 87
$ python3 vectorsum.py 2000
The last 2 elements of the sum [7980015996, 7992002000]
PythonSum elapsed time in microseconds 1623
The last 2 elements of the sum [7980015996 7992002000]
NumPySum elapsed time in microseconds 143
$ python3 vectorsum.py 4000
The last 2 elements of the sum [63920031996, 63968004000]
PythonSum elapsed time in microseconds 3417
The last 2 elements of the sum [63920031996 63968004000]
NumPySum elapsed time in microseconds 237
显而易见,NumPy的运行速度比等价的常规Python代码要快很多。有一件事情是肯定的:无论是否使用NumPy,计算结果都是相同的。但是结果的显示形式还是有所差别的,numpysum()函数给出的结果不包含逗号。为什么会这样?别忘了,我们处理的不是Python的列表,而是一个NumPy数组。有关NumPy数组的更多内容,将在第2章中详细介绍。
表1-1列出了在本章中讨论过的Python数据分析库的文档网站。
表1-1
软件包 |
描述 |
---|---|
|
NumPy和SciPy的主要文档网站是http://docs.scipy.org/doc/。通过该网站,您可以浏览NumPy和SciPy程序库的用户指南和参考指南,以及一些相关教程 |
|
http://pandas.pydata.org/pandas-docs/stable/ |
|
http://matplotlib.org/contents.html |
|
http://ipython.readthedocs.io/en/stable/ |
|
http://jupyter-notebook.readthedocs.io/en/latest/ |
流行的软件开发论坛Stack Overflow上也有数以百计的NumPy、SciPy、Pandas、Matplotlib、IPython和Jupyter Notebook方面的讨论。如果读者对这些内容感兴趣,建议进一步学习。
如果你遇到了比较棘手的问题,或者想要持续关注这些程序库的开发进展,可以订阅相应的讨论邮寄列表。订阅后,每天收到的数量不一的邮件,开发者会积极报告这些库的开发进展并热心回答其中的问题。
对于IRC用户,可以在irc://irc.freenode.net找到一个相关的频道,虽然该频道的名字是#scipy,但是这并不妨碍我们提问NumPy方面的问题,因为SciPy用户一般比较熟悉NumPy,毕竟SciPy是以NumPy为基础的。在这个SciPy频道中,通常有50多位成员保持在线。
ch-01.ipynb文件包含用于查看NumPy、SciPy、Pandas和Matplotlib库中的模块的代码。现在,读者不用担心这些代码的含义,只要尝试运行一下它们就行了。您可以修改其中的代码以查看其他库中的模块。
我们将在后面的章节中详细介绍数据的可视化问题。现在,我们先来加载两个样本数据集并生成一些简单的图形。首先,我们使用以下命令将sklearn库安装到要提供加载数据的地方。
$ pip3 install scikit-learn
接下来,使用以下命令导入数据集。
from sklearn.datasets import load_iris
from sklearn.datasets import load_boston
然后,导入Matplotlib绘图模块。
from matplotlib import pyplot as plt
%matplotlib inline
加载iris数据集,显示数据集的相关描述,同时将第1列(萼片长度)作为x坐标值,将第2列(萼片宽度)作为y坐标值。
iris = load_iris()
print(iris.DESCR)
data=iris.data
plt.plot(data[:,0],data[:,1],".")
这时,将生成如图1-3所示的图形。
图1-3
加载波士顿数据集,显示数据集的相关描述,同时将第3列(非零售业务的比例)作为x坐标值,将第5列(一氧化氮浓度)做为y坐标值,图上的每个点用“+”号表示。
boston = load_boston()
print(boston.DESCR)
data=boston.data
plt.plot(data[:,2],data[:,4],"+")
最终得到的图形如图1-4所示。
图1-4
本章安装了以后要用到的NumPy、SciPy、Pandas、Matplotlib、IPython和Jupyter Notebook等程序库,并通过一个向量加法程序,体验了NumPy带来的卓越性能。此外,我们还探讨了有关的文档和在线资源。同时,我们还尝试通过运行代码来查找库中的模块,并加载了一些样本数据集,还使用Matplotlib绘制一些简单的图形。
第2章将揭晓与NumPy有关的内容,以探索数组和数据类型等基本概念。
在前面我们已经尝试了一个基于SciPy栈中的基础数据分析程序库的实际代码。在本章中,我们将带领大家一起学习NumPy数组,熟悉NumPy数组的基础知识。阅读本章后,你会对NumPy数组及其相关函数有基本了解。
本章涉及的主题如下。
你可以在Jupyter Notebook中打开ch-02.ipynb文件,按照本章中的示例进行操作,此外也可以新建一个notebook,手动输入相应的代码。
NumPy提供了一个名为ndarray的多维数组对象。NumPy数组是具有固定大小的类型化数组。Python的列表是异构的,因此列表的元素可以包含任何对象类型,而NumPy数组是同质的,只能存放同一种类型的对象。数组由两部分组成,分别如下。
实际数据存储在连续的内存块中,因此当大型数据集作为ndarray进行加载时,我们就要看有没有足够多的连续内存块了。NumPy中的大部分数组方法和函数都不会直接修改实际数据,而只能修改元数据。
在之前的章节中,我们曾经用arange()函数来生成数组。实际上,那是用来存放一组数值的一维数组,这里的ndarray则可以具有一个以上的维度。
NumPy数组通常是同质的(有一种特殊的记录数组类型,它可以是异质的),即数组中的数据项的类型必须一致。NumPy数组元素类型一致的好处是:因为知道数组元素的类型相同,所以能轻松确定存储数组所需空间的大小。同时,NumPy数组还能够运用向量化运算来处理整个数组;而完成同样的任务,Python的列表则通常需要借助循环语句遍历列表并对逐个元素进行相应的处理。NumPy数组的索引方法与Python类似,下标从0开始。此外,NumPy使用了优化过的C API,所以运算速度格外快。
今后,我们会经常利用arange()子例程来建立数组。本章中的代码片断大部分取自Jupyter Notebook会话,而且在这些对话中,NumPy已经利用import numpy as np命令导入进来了。以下代码展示了如何获得数组的数据类型。
In: a = np.arange(5)
In: a.dtype
Out: dtype('int64')
以上数组的数据类型为int64(至少在作者的电脑上是这样的),不过如果你的Python为32位版本的话,得到的结果将是int32。无论上面哪一种情况,都是在处理整型变量(64位或者32位)。对于数组,除了要知道数据类型外,我们还要注意其形状,这一点非常重要。在第1章中,我们曾经举例说明向量(一维NumPy数组)的创建方法。数学家会经常用到向量,对我们来说,最常用的却是更高维度的对象。下面来看刚刚生成的那个向量的形状。
In: a
Out: array([0, 1, 2, 3, 4])
In: a.shape
Out: (5,)
如你所见,该向量有5个元素,它们的值分别是从0~4。该数组的shape属性是一个元组,就本例而言,这是一个单元素元组,存放的是数组在每一个维度的长度。
既然我们已经知道创建向量的方法,下面开始学习如何建立多维NumPy数组。生成矩阵后,我们再来看它的形状,代码如下。
(1)我们创建多维数组,代码如下。
In: m = np.array([np.arange(2), np.arange(2)])
In: m
Out:
array([[0, 1],
[0, 1]])
(2)显示该数组的形状的代码如下。
In: m.shape
Out: (2, 2)
上面我们用arange()子例程直接建立了一个2×2的数组,而利用array()函数创建数组时,则需要传递给它一个对象,同时这个对象还必须是数组类型的,如Python的列表。在上面的例子中,我们传给它的是由两个数组组成的一个列表。该对象是array()函数唯一所需的参数,而NumPy的函数往往有多个可选参数,而且这些参数都带有预定义的缺省选项。
有时我们可能想从数组中选择指定的元素。如何做到这一点呢?我们不妨从创建一个2×2矩阵着手。
In: a = np.array([[1,2],[3,4]])
In: a
Out:
array([[1, 2],
[3, 4]])
上面的矩阵是通过向array()函数传递一个由列表组成的列表得到的。接下来我们要逐个选择矩阵的各个元素,代码如下。别忘了下标是从0开始的。
In: a[0,0]
Out: 1
In: a[0,1]
Out: 2
In: a[1,0]
Out: 3
In: a[1,1]
Out: 4
可见,选择数组元素是一件非常简单的事情。对于数组a,我们只要通过a[m,n]的形式,就能访问数组内的元素,其中m和n为数组元素的下标。数组元素的下标如图2-1所示。
图2-1
Python自身虽然支持整型、浮点型和复数型,但对于科学计算来说,还远远不够。现实中,我们仍然需要更多的数据类型来满足在精度和存储大小方面的各种不同要求。为此,NumPy提供了更加丰富的数据类型。注意,NumPy中跟数学运算有关的数据类型的名称都以数字结尾。而这个数字指示了该类型的变量所占用的二进制位数。表2-1(改编自《NumPy用户指南》)概述了NumPy的各种数值类型。
表2-1
类型 |
说明 |
---|---|
|
布尔型(值为True或False),占用1bit |
|
其长度取决于平台的整数(通常为int32或者int64) |
|
字节类型(取值范围从−128~127) |
|
整型(取值范围−32768~32767) |
|
整型(取值范围为−231~231−1) |
|
整型(取值范围为−263~263−1) |
|
无符号整型(取值范围为0~255) |
|
无符号整型(取值范围为0~65535) |
|
无符号整型(取值范围为0~232−1) |
|
无符号整型(取值范围为0~264−1) |
|
半精度浮点型:符号占用1bit,指数占用5 bit,尾数占用10 bit |
|
单精度浮点型:符号占用1 bit,指数占用8 bit,尾数占用23 bit |
|
双精度浮点型:符号占用1 bit,指数占用11 bit,尾数占用52 bit |
|
复数类型,由两个32位浮点数(实部和虚部)表示 |
|
复数类型,由两个64位浮点数(实部和虚部)表示 |
每一种数据类型都有相应的转换函数,写法如下。
In: np.float64(42)
Out: 42.0
In: np.int8(42.0)
Out: 42
In: np.bool(42)
Out: True
In: np.bool(0)
Out: False
In: np.bool(42.0)
Out: True
In: np.float(True)
Out: 1.0
In: np.float(False)
Out: 0.0
许多函数都带有一个指定数据类型的参数,该参数通常是可选的。
In: np.arange(7, dtype= np.uint16)
Out: array([0, 1, 2, 3, 4, 5, 6], dtype=uint16)
切记:不能把复数类型转化成整型。当我们试图进行这种转换时,将会触发TypeError错误,就像下面这样。
In: np.int(42.0 + 1.j)Traceback (most recent call last):
<ipython-input-24-5c1cd108488d> in <module>()
----> 1 np.int(42.0 + 1.j)
TypeError: can't convert complex to int
同样,也不允许把复数转化成浮点数。另外,复数的分量j是其虚部的系数。不过,允许把浮点数转换成复数,如complex(1.0)是合法的。复数的实部和虚部分别使用real()函数和imag()函数提取。
数据类型对象是numpy.dtype类的实例。再强调一次,数组也有数据类型。严格地讲,NumPy数组中的每个元素都要具有相同的数据类型。数据类型对象表明了数据占用的字节数。所占用字节的具体数目一般存放在类dtype的itemsize属性中。
In: a.dtype.itemsize
Out: 8
NumPy之所以提供字符码,是为了与其前身Numeric向后兼容。一般我们不建议使用字符码,这里为什么又提供这些代码呢?因为我们会在许多地方碰到它们,但是,编写代码时我们应当使用dtype对象。表2-2展示了常见的一些数据类型及其相应的字符码。
表2-2
类型 |
字符码 |
---|---|
整型 |
|
无符号整型 |
|
单精度浮点型 |
|
双精度浮点型 |
|
布尔型 |
|
复数型 |
|
字符串 |
|
万国码(unicode) |
|
空类型(Void) |
|
下面的代码将生成一个单精度浮点型的数组。
In: arange(7, dtype='f')
Out: array([ 0., 1., 2., 3., 4., 5., 6.], dtype=float32)
类似地,下列代码将创建一个负数类型的数组。
In: arange(7, dtype='D')
Out: array([ 0.+0.j, 1.+0.j, 2.+0.j, 3.+0.j, 4.+0.j, 5.+0.j,
6.+0.j])
创建数据类型时手段有很多,下面我们以浮点型数据为例进行说明(以下代码取自本书代码包中的dtypeconstructors.py文件)。
In: np.dtype(float)
Out: dtype('float64')
In: np.dtype('f')
Out: dtype('float32')
In: np.dtype('d')
Out: dtype('float64')
In: np.dtype('f8')
Out: dtype('float64')
我们可以通过sctypeDict.keys()函数列出所有数据类型的字符码,代码如下。
In: np.sctypeDict.keys()
Out: dict_keys(['?', 0, 'byte', 'b', 1, 'ubyte', 'B', 2, 'short', 'h', 3,
'ushort', 'H', 4, 'i', 5, 'uint', 'I', 6, 'intp', 'p', 7, 'uintp', 'P', 8,
'long', 'l', 'L', 'longlong', 'q', 9, 'ulonglong', 'Q', 10, 'half', 'e',
23, 'f', 11, 'double', 'd', 12, 'longdouble', 'g', 13, 'cfloat', 'F', 14,
'cdouble', 'D', 15, 'clongdouble', 'G', 16, 'O', 17, 'S', 18, 'unicode',
'U', 19, 'void', 'V', 20, 'M', 21, 'm', 22, 'bool8', 'Bool', 'b1',
'float16', 'Float16', 'f2', 'float32', 'Float32', 'f4', 'float64',
'Float64', 'f8', 'float128', 'Float128', 'f16', 'complex64', 'Complex32',
'c8', 'complex128', 'Complex64', 'c16', 'complex256', 'Complex128', 'c32',
'object0', 'Object0', 'bytes0', 'Bytes0', 'str0', 'Str0', 'void0', 'Void0',
'datetime64', 'Datetime64', 'M8', 'timedelta64', 'Timedelta64', 'm8',
'int64', 'uint64', 'Int64', 'UInt64', 'i8', 'u8', 'int32', 'uint32',
'Int32', 'UInt32', 'i4', 'u4', 'int16', 'uint16', 'Int16', 'UInt16', 'i2',
'u2', 'int8', 'uint8', 'Int8', 'UInt8', 'i1', 'u1', 'complex_', 'int0',
'uint0', 'single', 'csingle', 'singlecomplex', 'float_', 'intc', 'uintc',
'int_', 'longfloat', 'clongfloat', 'longcomplex', 'bool_', 'unicode_',
'object_', 'bytes_', 'str_', 'string_', 'int', 'float', 'complex', 'bool',
'object', 'str', 'bytes', 'a'])
类dtype提供了许多有用的属性,如可以通过dtype的属性获取某种数据类型对应的字符码。
In: t = np.dtype('Float64')
In: t.char
Out: 'd'
类型属性相当于数组元素对象的类型,如下所示。
In: t.type
Out: numpy.float64
dtype的属性str中保存的是一个表示数据类型的字符串,其中第一个字符描述字节顺序,如果需要,后面会跟着字符码和数字,用来表示存储每个数组元素所需的字节数。这里,字节顺序(endianness)规定了32位或64位字内部各个字节的存储顺序。对于大端(big-endian)顺序,先存放权重最高的字节,用符号“>”指出;当使用小端(little-endian)顺序时,则存放权重最低的字节,用符号“<”指出。下面以代码为例进行说明。
In: t.str
Out: '<f8'
一维NumPy数组的切片操作与Python列表的切片一样。下面先来定义包含数字0、1、2, …, 8的一个数组,然后通过指定下标3~7来选择该数组的部分元素,这实际上就是提取数组中值为3~6的那些元素。
In: a = np.arange(9)
In: a[3:7]
Out: array([3, 4, 5, 6])
我们可以用下标选择元素,下标范围从0~7,而且下标每次递增2,如下所示。
In: a[:7:2]
Out: array([0, 2, 4, 6])
正如使用Python那样,我们也可用负值下标来反转数组。
In: a[::-1]
Out: array([8, 7, 6, 5, 4, 3, 2, 1, 0])
前面我们学习过reshape()函数,实际上,除了数组形状的调整外,数组的扩充也是一个经常碰到的乏味工作。比如,我们可以想象一下将多维数组转换成一维数组时的情形。我们创建一个数组b,下面的多个例子都会用到它。
In: b = np.arange(24).reshape(2,3,4)
In: print(b)
Out: [[[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]],
[[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23]]]
我们可以利用以下函数处理数组的形状。
In: b
Out:
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11]],
[[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23]]])
In: b.ravel()
Out:
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
14, 15, 16, 17, 18, 19, 20, 21, 22, 23])
In: b.flatten()
Out:
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
14, 15, 16, 17, 18, 19, 20, 21, 22, 23])
In: b.shape = (6,4)
In: b
Out:
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23]])
In: b.transpose()
Out:
array([[ 0, 4, 8, 12, 16, 20],
[ 1, 5, 9, 13, 17, 21],
[ 2, 6, 10, 14, 18, 22],
[ 3, 7, 11, 15, 19, 23]])
In: b.resize((2,12))
In: b
Out:
array([[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
[12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]])
从深度看,数组既可以横向叠放,也可以竖向叠放。为此,可以使用vstack()、dstack()、hstack()、column_stack()、row_stack()和concatenate()等函数。在此之前,我们先要建立某些数组。
In: a = np.arange(9).reshape(3,3)
In: a
Out:
array([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
In: b = 2 * a
In: b
Out:
array([[ 0, 2, 4],
[ 6, 8, 10],
[12, 14, 16]])
就像前面所说的,可以用下列技术来堆放数组。
In: np.hstack((a, b))
Out:
array([[ 0, 1, 2, 0, 2, 4],
[ 3, 4, 5, 6, 8, 10],
[ 6, 7, 8, 12, 14, 16]])
In: np.concatenate((a, b), axis=1)
Out:
array([[ 0, 1, 2, 0, 2, 4],
[ 3, 4, 5, 6, 8, 10],
[ 6, 7, 8, 12, 14, 16]])
水平叠加过程的示意图如图2-2所示。
图2-2
In: np.vstack((a, b))
Out:
array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 0, 2, 4],
[ 6, 8, 10],
[12, 14, 16]])
当参数axis设置为0时,concatenate()函数也会得到同样的效果。实际上,这是该参数的缺省值,代码如下。
In: np.concatenate((a, b), axis=0)
Out:
array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 0, 2, 4],
[ 6, 8, 10],
[12, 14, 16]])
垂直叠加过程的示意图如图2-3所示。
图2-3
In: np.dstack((a, b))
Out:
array([[[ 0, 0],
[ 1, 2],
[ 2, 4]],
[[ 3, 6],
[ 4, 8],
[ 5, 10]],
[[ 6, 12],
[ 7, 14],
[ 8, 16]]])
In: oned = np.arange(2)
In: oned
Out: array([0, 1])
In: twice_oned = 2 * oned
In: twice_oned
Out: array([0, 2])
In: np.column_stack((oned, twice_oned))
Out:
array([[0, 0],
[1, 2]])
用这种方法堆叠二维数组时,过程类似于hstack()函数,代码如下。
In: np.column_stack((a, b))
Out:
array([[ 0, 1, 2, 0, 2, 4],
[ 3, 4, 5, 6, 8, 10],
[ 6, 7, 8, 12, 14, 16]])
In: np.column_stack((a, b)) == np.hstack((a, b))
Out:
array([[ True, True, True, True, True, True],
[ True, True, True, True, True, True],
[ True, True, True, True, True, True]],
dtype=bool)
是的,你猜得没错!我们用“==”运算符对两个数组进行了比对。
In: np.row_stack((oned, twice_oned))
Out:
array([[0, 1],
[0, 2]])
对于二维数组,row_stack()函数相当于vstack()函数,代码如下。
In: np.row_stack((a, b))
Out:
array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 0, 2, 4],
[ 6, 8, 10],
[12, 14, 16]])
In: np.row_stack((a,b)) == np.vstack((a, b))
Out:
array([[ True, True, True],
[ True, True, True],
[ True, True, True],
[ True, True, True],
[ True, True, True],
[ True, True, True]], dtype=bool)
我可以从纵向、横向和深度方向来拆分数组,相关函数有hsplit()、vsplit()、dsplit()和split()。我们既可以把数组分成相同形状的数组,也可以从规定的位置开始切取数组。下面对相关函数逐个详解。
In: a
Out:
array([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
In: np.hsplit(a, 3)
Out:
[array([[0],
[3],
[6]]),
array([[1],
[4],
[7]]),
array([[2],
[5],
[8]])]
这相当于调用了参数axis=1的split()函数。
In: np.split(a, 3, axis=1)
Out:
[array([[0],
[3],
[6]]),
array([[1],
[4],
[7]]),
array([[2],
[5],
[8]])]
In: np.vsplit(a, 3)
Out: [array([[0, 1, 2]]), array([[3, 4, 5]]),
array([[6, 7, 8]])]
当参数axis=0时,split()函数也会沿着纵轴方向分解数组,代码如下
In: np.split(a, 3, axis=0)
Out: [array([[0, 1, 2]]), array([[3, 4, 5]]),
array([[6, 7, 8]])]
In: c = np.arange(27).reshape(3, 3, 3)
In: c
Out:
array([[[ 0, 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]]])
In: np.dsplit(c, 3)
Out:
[array([[[ 0],
[ 3],
[ 6]],
[[ 9],
[12],
[15]],
[[18],
[21],
[24]]]),
array([[[ 1],
[ 4],
[ 7]],
[[10],
[13],
[16]],
[[19],
[22],
[25]]]),
array([[[ 2],
[ 5],
[ 8]],
[[11],
[14],
[17]],
[[20],
[23],
[26]]])]
下面举例说明NumPy数组各种属性的详细用法。我们创建一个数组b,以便后面的例子使用。
In: b = np.arange(24).reshape(2, 12)
In: b
Out:
array([[ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
[12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]])
除shape和dtype属性外,ndarray类型的属性还很多,下面逐一列出。
In: b.ndim
Out: 2
In: b.size
Out: 24
In: b.itemsize
Out: 8
In: b.nbytes
Out: 192
In: b.size * b.itemsize
Out: 192
In: b.resize(6,4)
In: b
Out:
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23]])
In: b.T
Out:
array([[ 0, 4, 8, 12, 16, 20],
[ 1, 5, 9, 13, 17, 21],
[ 2, 6, 10, 14, 18, 22],
[ 3, 7, 11, 15, 19, 23]])
In: b.ndim
Out: 1
In: b.T
Out: array([0, 1, 2, 3, 4])
In: b = np.array([1.j + 1, 2.j + 3])
In: b
Out: array([ 1.+1.j, 3.+2.j])
In: b.real
Out: array([ 1., 3.])
In: b.imag
Out: array([ 1., 2.])
In: b.dtype
Out: dtype('complex128')
In: b.dtype.str
Out: '<c16'
In: b = np.arange(4).reshape(2,2)
In: b
Out:
array([[0, 1],
[2, 3]])
In: f = b.flat
In: f
Out: <numpy.flatiter object at 0x103013e00>
In: for item in f: print(item)
Out:
0
1
2
3
当然,取得flatiter对象的元素也不难,代码如下。
In: b.flat[2]
Out: 2
此外,还可以请求多个元素,代码如下。
In: b.flat[[1,3]]
Out: array([1, 3])
同时,还可以给flat属性赋值。不过,需要注意的是,这个值将会覆盖整个数组内所有元素的值,下面举例说明。
In: b.flat = 7
In: b
Out:
array([[7, 7],
[7, 7]])
此外,还可以返回指定的元素,代码如下。
In: b.flat[[1,3]] = 1
In: b
Out:
array([[7, 1],
[7, 1]])
图2-4是对ndarray各种属性的一个小结。
图2-4
我们可以把NumPy数组转换成Python列表,使用tolist()函数即可。下面简单解释一下。
In: b
Out: array([ 1.+1.j, 3.+2.j])
In: b.tolist()
Out: [(1+1j), (3+2j)]
In: b
Out: array([ 1.+1.j, 3.+2.j])
In: b.astype(int)
/usr/local/lib/python3.5/site-packages/ipykernel/__main__.py:1:
ComplexWarning: Casting complex values to real discards the
imaginary part
...
Out: array([1, 3])
In: b.astype('complex')
Out: array([ 1.+1.j, 3.+2.j])
提示
当complex类型转换成int类型时,虚部将被丢弃。另外,我们还需要将数据类型的名称以字符串的形式传递给astype()函数。
上述代码没有显示警告信息,因为这次使用的是正确的数据类型。
在介绍ravel()函数的示例中,我们提到了视图的概念。不过,请不要与数据库中的视图概念混淆。在NumPy的世界里,视图不是只读的,因为你不可能守着基础数据一动不动。关键在于要知道当前处理的是共享的数组视图还是数组数据的副本。举例来说,我们可以取数组的一部分来生成视图。这意味着,如果先将数组的某部分赋给一个变量,然后修改原数组中相应位置的数据,那么这个变量的值也会随之变化。我们可以根据SciPy包中的面部照片来创建数组,然后创建视图,随后修改它。这里,莱娜肖像的数组是从SciPy函数获得的。
(1)获取面部图片。
face = scipy.misc.face()
(2)为该面部数组创建副本。
acopy = face.copy()
(3)为该数组创建一个视图。
aview = face.view()
(4)通过flat迭代器将视图中所有的值全部设为0。
aview.flat = 0
最后,只有一幅图片可以看到图像,而其他图片根本看不到什么,如图2-5所示。
图2-5
下面的代码很好地展示了数组的视图和副本的特点。
import scipy.misc
import matplotlib.pyplot as plt
face = scipy.misc.face()
acopy = face.copy()
aview = face.view()
aview.flat = 0
plt.subplot(221)
plt.imshow(face)
plt.subplot(222)
plt.imshow(acopy)
plt.subplot(223)
plt.imshow(aview)
plt.show()
可见,在程序结束部分修改视图,同时改变了原来的莱娜数组。这导致3副图片全部变蓝(如果阅读的是本书的印刷版,也可能显示为黑色),复制的数组则没有任何变化。所以一定要记住:视图不是只读的。
花式索引是一种传统的索引方法,它不使用整数或者切片。这里,我们将利用花式索引把莱娜照片对角线上的值全部设置为0,相当于沿着两条交叉的对角线画两条黑线。
下面我们给出本例中的相应代码。
import scipy.misc
import matplotlib.pyplot as plt
face = scipy.misc.face()
xmax = face.shape[0]
ymax = face.shape[1]
face=face[:min(xmax,ymax),:min(xmax,ymax)]
xmax = face.shape[0]
ymax = face.shape[1]
face[range(xmax), range(ymax)] = 0
face[range(xmax-1,-1,-1), range(ymax)] = 0
plt.imshow(face)
plt.show()
下面我们对上述代码进行简单说明。
(1)将第一条对角线上的值设为0。
为了将对角线上的值设置为0,我们需要给x和y值(直角坐标系中的坐标)规定两个不同的范围。
face[range(xmax), range(ymax)] = 0
(2)将另一条对角线上的值设为0。
要设置另一条对角线上的值,需要规定两个不同的取值范围,但是规则不变。
face[range(xmax-1,-1,-1), range(ymax)] = 0
划掉相片对角线后,最后得到图2-6所示的效果。
图2-6
我们给x和y规定了不同的取值范围,这些范围用来索引莱娜数组。花式索引是在一个内部的NumPy迭代器对象的基础上实现的,分3步完成。
(1)创建迭代器对象。
(2)将迭代器对象绑定到数组。
(3)经由迭代器访问数组元素,利用位置列表进行索引。
下面利用ix_()函数将莱娜照片中的像素完全打乱。注意,本例中的代码没有提供注释。完整的代码请参考本书代码包中的ix.py文件。
import scipy.misc
import matplotlib.pyplot as plt
import numpy as np
face = scipy.misc.face()
xmax = face.shape[0]
ymax = face.shape[1]
def shuffle_indices(size):
arr = np.arange(size)
np.random.shuffle(arr)
return arr
xindices = shuffle_indices(xmax)
np.testing.assert_equal(len(xindices), xmax)
yindices = shuffle_indices(ymax)
np.testing.assert_equal(len(yindices), ymax)
plt.imshow(face[np.ix_(xindices, yindices)])
plt.show()
这个函数可以根据多个序列生成一个网格,它需要一个一维序列作为参数并返回一个由NumPy数组构成的元组。
In : ix_([0,1], [2,3])
Out:
(array([[0],[1]]), array([[2, 3]]))
利用位置列表索引NumPy数组的过程如下。
(1)打乱数组的索引。
我们用numpy.random子程序包中的shuffle()函数把数组中的元素按随机的索引号重新排列,使得数组产生相应的变化。
def shuffle_indices(size):
arr = np.arange(size)
np.random.shuffle(arr)
return arr
(2)使用下面的代码画出打乱后的索引。
plt.imshow( face[np.ix_(xindices, yindices)])
(3)莱娜照片的像素被完全打乱后,变成图2-7所示的样子。
图2-7
布尔型索引是指根据布尔型数组来索引元素的方法,属于花式索引系列。因为布尔型索引是花式索引的一个分类,所以它们的使用方法基本相同。
下面用代码(详见本书代码包中的boolean_indexing.py文件)具体演示其使用方法。
import scipy.misc
import matplotlib.pyplot as plt
import numpy as np
face = scipy.misc.face()
xmax = face.shape[0]
ymax = face.shape[1]
face=face[:min(xmax,ymax),:min(xmax,ymax)]
def get_indices(size):
arr = np.arange(size)
return arr % 4 == 0
face1 = face.copy()
xindices = get_indices(face.shape[0])
yindices = get_indices(face.shape[1])
face1[xindices, yindices] = 0
plt.subplot(211)
plt.imshow(face1)
face2 = face.copy()
face2[(face > face.max()/4) & (face < 3 * face.max()/4)] = 0
plt.subplot(212)
plt.imshow(face2)
plt.show()
上述代码利用一种特殊的迭代器对象来索引元素,下面进行简单说明。
(1)在对角线上画点。
这类似于花式索引,不过这里选择的是照片对角线上可以被4整除的那些位置上的点。
def get_indices(size):
arr = np.arange(size)
return arr % 4 == 0
然后我们仅绘出选定的那些点。
face1 = face.copy()
xindices = get_indices(face.shape[0])
yindices = get_indices(face.shape[1])
face1[xindices, yindices] = 0
plt.subplot(211)
plt.imshow(face1)
(2)根据元素值的情况置零。
选取数组值介于最大值的1/4~3/4的那些元素,将其设置为0。
face2[(face > face.max()/4) & (face < 3 * face.max()/4)] = 0
(3)两幅新照片如图2-8所示。
图2-8
当操作对象的形状不一样时,NumPy会尽力进行处理。
例如,假设一个数组要跟一个标量相乘,这时标量需要根据数组的形状进行扩展,然后才可以执行乘法运算。这个扩展的过程叫作广播(broadcasting)。下面用代码加以说明。
import scipy.io.wavfile as sw
import matplotlib.pyplot as plt
import urllib
import numpy as np
request =
urllib.request.Request('http://www.thesoundarchive.com/austinpowers/smashin
gbaby.wav')
response = urllib.request.urlopen(request)
print(response.info())
WAV_FILE = 'smashingbaby.wav'
filehandle = open(WAV_FILE, 'wb')
filehandle.write(response.read())
filehandle.close()
sample_rate, data = sw.read(WAV_FILE)
print("Data type", data.dtype, "Shape", data.shape)
plt.subplot(2, 1, 1)
plt.title("Original")
plt.plot(data)
newdata = data * 0.2
newdata = newdata.astype(np.uint8)
print("Data type", newdata.dtype, "Shape", newdata.shape)
sw.write("quiet.wav", sample_rate, newdata)
plt.subplot(2, 1, 2)
plt.title("Quiet")
plt.plot(newdata)2
plt.show()
下面,我们将下载一个音频文件,然后以此为基础,生成一个新的静音版本。
(1)读取WAV文件。
我们将使用标准的Python代码来下载电影《王牌大贱谍》(Austin Powers)中的狂嚎式歌曲《Smashing, baby》的声音文件。SciPy中有一个wavfile子程序包,可以用来加载音频数据,或者生成WAV格式的文件。如果此前已经安装了SciPy,那么现在就可以直接使用这个子程序包了。我们可以使用函数read()读取文件,它返回一个数据阵列及采样率,不过,这里只对数据本身感兴趣。
sample_rate, data = scipy.io.wavfile.read(WAV_FILE)
(2)绘制原WAV数据。
这里,我们利用matplotlib绘制原始WAV数据并用一个子图来显示标题“Original”,代码如下。
plt.subplot(2, 1, 1)
plt.title("Original")
plt.plot(data)
(3)新建一个数组。
现在,我们要用NumPy来生成一段“寂静的”声音。实际上就是将原数组的值乘以一个常数,从而得到一个新数组,因为这个新数组的元素值肯定是变小了。这正是广播技术的用武之地。最后,我们要确保新数组与原数组的类型一致,即WAV格式。
newdata = data * 0.2
newdata = newdata.astype(np.uint8)
(4)写入一个WAV文件中。
我们将新数组保存到一个新的WAV文件中,代码如下。
scipy.io.wavfile.write("quiet.wav",
sample_rate, newdata)
(5)绘制出新的WAV数据。
我们可以使用matplotlib来画出新数组中的数据,如下所示。
plt.subplot(2, 1, 2)
plt.title("Quiet")
plt.plot(newdata)
plt.show()
图2-9展示了原始的WAV文件中的数据的图像以及数值变小后新数组的图像。
图2-9
本章,我们学习了NumPy的基础知识:数据类型和数组。数组具有许多属性,这些属性都是用来描述该数组的特性的。我们探讨的属性之一便是数据类型,实际上,NumPy是通过一个成熟完备的对象来表示这个属性的。
与Python标准的列表相比,NumPy数组使用的切片和索引方法更加高效。此外,NumPy数组还能够对多维度数组进行处理。
我们可以用各种方式改变数组的形状,如堆叠、重定尺寸、重塑形状以及拆分等。在本章中,我们还为处理数组的形状介绍了许多简便易用的函数。
有了这些基础知识后,从第4章开始,我们就要学习如何通过常见的函数来分析数据了,这将涉及主要统计函数和数值函数的用法。
我们鼓励读者阅读“参考文献”部分中提到的书籍,以拓展大家在NumPy库知识方面的深度和广度。
1.I. Idris, NumPy Cookbook – Second edition, Packt Publishing, 2015.
2.I. Idris, Learning NumPy Array, Packt Publishing, 2014.
3.I. Idris, Numpy: Beginners Guide – Third Edition, Packt Publishing, 2015.
4.L. (Liang-H.) Chin and T. Dutta, NumPy Essentials, Packt Publishing, 2016.