【翻译】《利用Python进行数据分析·第2版》第4章(上)NumPy基础:数组和矢量计算
作者:SeanCheney Python爱好者社区专栏作者
简书专栏:https://www.jianshu.com/u/130f76596b02
前文传送门:
【翻译】《利用Python进行数据分析·第2版》第1章 准备工作
【翻译】《利用Python进行数据分析·第2版》第2章(上)Python语法基础,IPython和Jupyter
【翻译】《利用Python进行数据分析·第2版》第2章(中)Python语法基础,IPython和Jupyter
【翻译】《利用Python进行数据分析·第2版》第2章(下)Python语法基础,IPython和Jupyter
【翻译】《利用Python进行数据分析·第2版》第3章(上)Python的数据结构、函数和文件
【翻译】《利用Python进行数据分析·第2版》第3章(中)Python的数据结构、函数和文件
【翻译】《利用Python进行数据分析·第2版》第3章(下)Python的数据结构、函数和文件
NumPy(Numerical Python的简称)是Python数值计算最重要的基础包。大多数提供科学计算的包都是用NumPy的数组作为构建基础。
NumPy的部分功能如下:
ndarray,一个具有矢量算术运算和复杂广播能力的快速且节省空间的多维数组。
用于对整组数据进行快速运算的标准数学函数(无需编写循环)。
用于读写磁盘数据的工具以及用于操作内存映射文件的工具。
线性代数、随机数生成以及傅里叶变换功能。
用于集成由C、C++、Fortran等语言编写的代码的A C API。
由于NumPy提供了一个简单易用的C API,因此很容易将数据传递给由低级语言编写的外部库,外部库也能以NumPy数组的形式将数据返回给Python。这个功能使Python成为一种包装C/C++/Fortran历史代码库的选择,并使被包装库拥有一个动态的、易用的接口。
NumPy本身并没有提供多么高级的数据分析功能,理解NumPy数组以及面向数组的计算将有助于你更加高效地使用诸如pandas之类的工具。因为NumPy是一个很大的题目,我会在附录A中介绍更多NumPy高级功能,比如广播。
对于大部分数据分析应用而言,我最关注的功能主要集中在:
用于数据整理和清理、子集构造和过滤、转换等快速的矢量化数组运算。
常用的数组算法,如排序、唯一化、集合运算等。
高效的描述统计和数据聚合/摘要运算。
用于异构数据集的合并/连接运算的数据对齐和关系型数据运算。
将条件逻辑表述为数组表达式(而不是带有if-elif-else分支的循环)。
数据的分组运算(聚合、转换、函数应用等)。。
虽然NumPy提供了通用的数值数据处理的计算基础,但大多数读者可能还是想将pandas作为统计和分析工作的基础,尤其是处理表格数据时。pandas还提供了一些NumPy所没有的更加领域特定的功能,如时间序列处理等。
笔记:Python的面向数组计算可以追溯到1995年,Jim Hugunin创建了Numeric库。接下来的10年,许多科学编程社区纷纷开始使用Python的数组编程,但是进入21世纪,库的生态系统变得碎片化了。2005年,Travis Oliphant从Numeric和Numarray项目整了出了NumPy项目,进而所有社区都集合到了这个框架下。
NumPy之于数值计算特别重要的原因之一,是因为它可以高效处理大数组的数据。这是因为:
NumPy是在一个连续的内存块中存储数据,独立于其他Python内置对象。NumPy的C语言编写的算法库可以操作内存,而不必进行类型检查或其它前期工作。比起Python的内置序列,NumPy数组使用的内存更少。
NumPy可以在整个数组上执行复杂的计算,而不需要Python的for循环。
要搞明白具体的性能差距,考察一个包含一百万整数的数组,和一个等价的Python列表:
In [7]: import numpy as np In [8]: my_arr = np.arange(1000000) In [9]: my_list = list(range(1000000))
各个序列分别乘以2:
In [10]: %time for _ in range(10): my_arr2 = my_arr * 2 CPU times: user 20 ms, sys: 50 ms, total: 70 ms Wall time: 72.4 ms In [11]: %time for _ in range(10): my_list2 = [x * 2 for x in my_list] CPU times: user 760 ms, sys: 290 ms, total: 1.05 s Wall time: 1.05 s
基于NumPy的算法要比纯Python快10到100倍(甚至更快),并且使用的内存更少。
4.1 NumPy的ndarray:一种多维数组对象
NumPy最重要的一个特点就是其N维数组对象(即ndarray),该对象是一个快速而灵活的大数据集容器。你可以利用这种数组对整块数据执行一些数学运算,其语法跟标量元素之间的运算一样。
要明白Python是如何利用与标量值类似的语法进行批次计算,我先引入NumPy,然后生成一个包含随机数据的小数组:
In [12]: import numpy as np # Generate some random data In [13]: data = np.random.randn(2, 3) In [14]: data Out[14]: array([[-0.2047, 0.4789, -0.5194], [-0.5557, 1.9658, 1.3934]])
然后进行数学运算:
In [15]: data * 10 Out[15]: array([[ -2.0471, 4.7894, -5.1944], [ -5.5573, 19.6578, 13.9341]]) In [16]: data + data Out[16]: array([[-0.4094, 0.9579, -1.0389], [-1.1115, 3.9316, 2.7868]])
第一个例子中,所有的元素都乘以10。第二个例子中,每个元素都与自身相加。
笔记:在本章及全书中,我会使用标准的NumPy惯用法
import numpy as np
。你当然也可以在代码中使用from numpy import *
,但不建议这么做。numpy
的命名空间很大,包含许多函数,其中一些的名字与Python的内置函数重名(比如min和max)。
ndarray是一个通用的同构数据多维容器,也就是说,其中的所有元素必须是相同类型的。每个数组都有一个shape(一个表示各维度大小的元组)和一个dtype(一个用于说明数组数据类型的对象):
In [17]: data.shape Out[17]: (2, 3) In [18]: data.dtype Out[18]: dtype('float64')
本章将会介绍NumPy数组的基本用法,这对于本书后面各章的理解基本够用。虽然大多数数据分析工作不需要深入理解NumPy,但是精通面向数组的编程和思维方式是成为Python科学计算牛人的一大关键步骤。
笔记:当你在本书中看到“数组”、“NumPy数组”、"ndarray"时,基本上都指的是同一样东西,即ndarray对象。
创建ndarray
创建数组最简单的办法就是使用array函数。它接受一切序列型的对象(包括其他数组),然后产生一个新的含有传入数据的NumPy数组。以一个列表的转换为例:
In [19]: data1 = [6, 7.5, 8, 0, 1] In [20]: arr1 = np.array(data1) In [21]: arr1 Out[21]: array([ 6. , 7.5, 8. , 0. , 1. ])
嵌套序列(比如由一组等长列表组成的列表)将会被转换为一个多维数组:
In [22]: data2 = [[1, 2, 3, 4], [5, 6, 7, 8]] In [23]: arr2 = np.array(data2) In [24]: arr2 Out[24]: array([[1, 2, 3, 4], [5, 6, 7, 8]])
因为data2是列表的列表,NumPy数组arr2的两个维度的shape是从data2引入的。可以用属性ndim和shape验证:
In [25]: arr2.ndim Out[25]: 2 In [26]: arr2.shape Out[26]: (2, 4)
除非特别说明(稍后将会详细介绍),np.array会尝试为新建的这个数组推断出一个较为合适的数据类型。数据类型保存在一个特殊的dtype对象中。比如说,在上面的两个例子中,我们有:
In [27]: arr1.dtype Out[27]: dtype('float64') In [28]: arr2.dtype Out[28]: dtype('int64')
除np.array之外,还有一些函数也可以新建数组。比如,zeros和ones分别可以创建指定长度或形状的全0或全1数组。empty可以创建一个没有任何具体值的数组。要用这些方法创建多维数组,只需传入一个表示形状的元组即可:
In [29]: np.zeros(10) Out[29]: array([ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]) In [30]: np.zeros((3, 6)) Out[30]: array([[ 0., 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0., 0.], [ 0., 0., 0., 0., 0., 0.]]) In [31]: np.empty((2, 3, 2)) Out[31]: array([[[ 0., 0.], [ 0., 0.], [ 0., 0.]], [[ 0., 0.], [ 0., 0.], [ 0., 0.]]])
注意:认为np.empty会返回全0数组的想法是不安全的。很多情况下(如前所示),它返回的都是一些未初始化的垃圾值。
arange是Python内置函数range的数组版:
In [32]: np.arange(15) Out[32]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
表4-1列出了一些数组创建函数。由于NumPy关注的是数值计算,因此,如果没有特别指定,数据类型基本都是float64(浮点数)。
表4-1 数组创建函数
ndarray的数据类型
dtype(数据类型)是一个特殊的对象,它含有ndarray将一块内存解释为特定数据类型所需的信息:
In [33]: arr1 = np.array([1, 2, 3], dtype=np.float64) In [34]: arr2 = np.array([1, 2, 3], dtype=np.int32) In [35]: arr1.dtype Out[35]: dtype('float64') In [36]: arr2.dtype Out[36]: dtype('int32')
dtype是NumPy灵活交互其它系统的源泉之一。多数情况下,它们直接映射到相应的机器表示,这使得“读写磁盘上的二进制数据流”以及“集成低级语言代码(如C、Fortran)”等工作变得更加简单。数值型dtype的命名方式相同:一个类型名(如float或int),后面跟一个用于表示各元素位长的数字。标准的双精度浮点值(即Python中的float对象)需要占用8字节(即64位)。因此,该类型在NumPy中就记作float64。表4-2列出了NumPy所支持的全部数据类型。
笔记:记不住这些NumPy的dtype也没关系,新手更是如此。通常只需要知道你所处理的数据的大致类型是浮点数、复数、整数、布尔值、字符串,还是普通的Python对象即可。当你需要控制数据在内存和磁盘中的存储方式时(尤其是对大数据集),那就得了解如何控制存储类型。
你可以通过ndarray的astype方法明确地将一个数组从一个dtype转换成另一个dtype:
In [37]: arr = np.array([1, 2, 3, 4, 5]) In [38]: arr.dtype Out[38]: dtype('int64') In [39]: float_arr = arr.astype(np.float64) In [40]: float_arr.dtype Out[40]: dtype('float64')
在本例中,整数被转换成了浮点数。如果将浮点数转换成整数,则小数部分将会被截取删除:
In [41]: arr = np.array([3.7, -1.2, -2.6, 0.5, 12.9, 10.1]) In [42]: arr Out[42]: array([ 3.7, -1.2, -2.6, 0.5, 12.9, 10.1]) In [43]: arr.astype(np.int32) Out[43]: array([ 3, -1, -2, 0, 12, 10], dtype=int32)
如果某字符串数组表示的全是数字,也可以用astype将其转换为数值形式:
In [44]: numeric_strings = np.array(['1.25', '-9.6', '42'], dtype=np.string_) In [45]: numeric_strings.astype(float) Out[45]: array([ 1.25, -9.6 , 42. ])
注意:使用numpy.string_类型时,一定要小心,因为NumPy的字符串数据是大小固定的,发生截取时,不会发出警告。pandas提供了更多非数值数据的便利的处理方法。
如果转换过程因为某种原因而失败了(比如某个不能被转换为float64的字符串),就会引发一个ValueError。这里,我比较懒,写的是float而不是np.float64;NumPy很聪明,它会将Python类型映射到等价的dtype上。
数组的dtype还有另一个属性:
In [46]: int_array = np.arange(10)
In [47]: calibers = np.array([.22, .270, .357, .380, .44, .50], dtype=np.float64)
In [48]: int_array.astype(calibers.dtype)
Out[48]: array([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])
你还可以用简洁的类型代码来表示dtype:
In [49]: empty_uint32 = np.empty(8, dtype='u4') In [50]: empty_uint32 Out[50]: array([ 0, 1075314688, 0, 1075707904, 0, 1075838976, 0, 1072693248], dtype=uint32)
笔记:调用astype总会创建一个新的数组(一个数据的备份),即使新的dtype与旧的dtype相同。
NumPy数组的运算
数组很重要,因为它使你不用编写循环即可对数据执行批量运算。NumPy用户称其为矢量化(vectorization)。大小相等的数组之间的任何算术运算都会将运算应用到元素级:
In [51]: arr = np.array([[1., 2., 3.], [4., 5., 6.]]) In [52]: arr Out[52]: array([[ 1., 2., 3.], [ 4., 5., 6.]]) In [53]: arr * arr Out[53]: array([[ 1., 4., 9.], [ 16., 25., 36.]]) In [54]: arr - arr Out[54]: array([[ 0., 0., 0.], [ 0., 0., 0.]])
数组与标量的算术运算会将标量值传播到各个元素:
In [55]: 1 / arr Out[55]: array([[ 1. , 0.5 , 0.3333], [ 0.25 , 0.2 , 0.1667]]) In [56]: arr ** 0.5 Out[56]: array([[ 1. , 1.4142, 1.7321], [ 2. , 2.2361, 2.4495]])
大小相同的数组之间的比较会生成布尔值数组:
In [57]: arr2 = np.array([[0., 4., 1.], [7., 2., 12.]]) In [58]: arr2 Out[58]: array([[ 0., 4., 1.], [ 7., 2., 12.]]) In [59]: arr2 > arr Out[59]: array([[False, True, False], [ True, False, True]], dtype=bool)
不同大小的数组之间的运算叫做广播(broadcasting),将在附录A中对其进行详细讨论。本书的内容不需要对广播机制有多深的理解。
基本的索引和切片
NumPy数组的索引是一个内容丰富的主题,因为选取数据子集或单个元素的方式有很多。一维数组很简单。从表面上看,它们跟Python列表的功能差不多:
In [60]: arr = np.arange(10) In [61]: arr Out[61]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) In [62]: arr[5] Out[62]: 5 In [63]: arr[5:8] Out[63]: array([5, 6, 7]) In [64]: arr[5:8] = 12 In [65]: arr Out[65]: array([ 0, 1, 2, 3, 4, 12, 12, 12, 8, 9])
如上所示,当你将一个标量值赋值给一个切片时(如arr[5:8]=12),该值会自动传播(也就说后面将会讲到的“广播”)到整个选区。跟列表最重要的区别在于,数组切片是原始数组的视图。这意味着数据不会被复制,视图上的任何修改都会直接反映到源数组上。
作为例子,先创建一个arr的切片:
In [66]: arr_slice = arr[5:8] In [67]: arr_slice Out[67]: array([12, 12, 12])
现在,当我修稿arr_slice中的值,变动也会体现在原始数组arr中:
In [68]: arr_slice[1] = 12345 In [69]: arr Out[69]: array([ 0, 1, 2, 3, 4, 12, 12345, 12, 8, 9])
切片[ : ]会给数组中的所有值赋值:
In [70]: arr_slice[:] = 64 In [71]: arr Out[71]: array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])
如果你刚开始接触NumPy,可能会对此感到惊讶(尤其是当你曾经用过其他热衷于复制数组数据的编程语言)。由于NumPy的设计目的是处理大数据,所以你可以想象一下,假如NumPy坚持要将数据复制来复制去的话会产生何等的性能和内存问题。
注意:如果你想要得到的是ndarray切片的一份副本而非视图,就需要明确地进行复制操作,例如
arr[5:8].copy()
。
对于高维度数组,能做的事情更多。在一个二维数组中,各索引位置上的元素不再是标量而是一维数组:
In [72]: arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) In [73]: arr2d[2] Out[73]: array([7, 8, 9])
因此,可以对各个元素进行递归访问,但这样需要做的事情有点多。你可以传入一个以逗号隔开的索引列表来选取单个元素。也就是说,下面两种方式是等价的:
In [74]: arr2d[0][2] Out[74]: 3 In [75]: arr2d[0, 2] Out[75]: 3
图4-1说明了二维数组的索引方式。轴0作为行,轴1作为列。
图4-1 NumPy数组中的元素索引
在多维数组中,如果省略了后面的索引,则返回对象会是一个维度低一点的ndarray(它含有高一级维度上的所有数据)。因此,在2×2×3数组arr3d中:
In [76]: arr3d = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]]) In [77]: arr3d Out[77]: array([[[ 1, 2, 3], [ 4, 5, 6]], [[ 7, 8, 9], [10, 11, 12]]])
arr3d[0]是一个2×3数组:
In [78]: arr3d[0] Out[78]: array([[1, 2, 3], [4, 5, 6]])
标量值和数组都可以被赋值给arr3d[0]:
In [79]: old_values = arr3d[0].copy() In [80]: arr3d[0] = 42 In [81]: arr3d Out[81]: array([[[42, 42, 42], [42, 42, 42]], [[ 7, 8, 9], [10, 11, 12]]]) In [82]: arr3d[0] = old_values In [83]: arr3d Out[83]: array([[[ 1, 2, 3], [ 4, 5, 6]], [[ 7, 8, 9], [10, 11, 12]]])
相似的,arr3d[1,0]可以访问索引以(1,0)开头的那些值(以一维数组的形式返回):
In [84]: arr3d[1, 0] Out[84]: array([7, 8, 9])
虽然是用两步进行索引的,表达式是相同的:
In [85]: x = arr3d[1] In [86]: x Out[86]: array([[ 7, 8, 9], [10, 11, 12]]) In [87]: x[0] Out[87]: array([7, 8, 9])
注意,在上面所有这些选取数组子集的例子中,返回的数组都是视图。
赞赏作者
Python爱好者社区历史文章大合集:
Python爱好者社区历史文章列表(每周append更新一次)
关注后在公众号内回复“课程”即可获取:
小编的Python入门视频课程!!!
崔老师爬虫实战案例免费学习视频。
丘老师数据科学入门指导免费学习视频。
陈老师数据分析报告制作免费学习视频。
玩转大数据分析!Spark2.X+Python 精华实战课程免费学习视频。
丘老师Python网络爬虫实战免费学习视频。