第 3 章 NumPy库

第 3 章 NumPy库

NumPy是用Python进行科学计算,尤其是数据分析时,所用到的一个基础库。它是大量Python数学和科学计算包的基础,比如后面要讲到的pandas库就用到了NumPy。pandas库专门用于数据分析,充分借鉴了Python标准库NumPy的相关概念。而Python标准库所提供的内置工具对数据分析方面的大多数计算来说都过于简单或不够用。

为了更好地理解和使用Python所有的科学计算包,尤其是pandas,需要先行掌握NumPy库的用法,这样才能把pandas的用处发挥到极致。pandas是后续章节的主题。

如果你已熟悉NumPy库,可跳过本章直接学习下一章;否则,你可以借此机会复习一下NumPy的基础概念,或者敲敲本章的示例代码,争取尽快使自己再次熟悉起来。

3.1 NumPy简史

Python语言诞生不久,开发人员就产生了数值计算的需求,更为重要的是,科学社区开始考虑用它进行科学计算。

1995年,Jim Hugunin开发了Numeric,这是第一次尝试用Python进行科学计算。随后又诞生了Numarray包。这两个包都是专门用于数组计算的,但各有各的优势,开发人员只好根据不同的使用场景,从中选择效率更高的包。由于两者之间的区别并不那么明确,开发人员产生了把它们整合为一个包的想法。Travis Oliphant遂着手开发NumPy库,并于2006年发布了它的第一个版本(v 1.0)。

从此之后,NumPy成为Python科学计算的扩展包。如今,在计算多维数组和大型数组方面,它是使用最广的。此外,它还提供多个函数,操作起数组来效率很高,还可用来实现高级数学运算。

当前,NumPy是开源项目,使用BSD许可证。在众多开发者的支持下,这个库的潜力得到了进一步挖掘。

3.2 NumPy安装

通常,大多数Python发行版都把NumPy作为一个基础包。然而,如果NumPy不是基础包的话,你可以自行安装。

Linux系统(Ubuntu和Debian):

sudo apt-get install python-numpy

Linux系统(Fedora):

sudo yum install numpy scipy

使用Anaconda发行版的Windows系统:

conda install numpy

NumPy安装到系统之后,在Python会话中输入以下代码导入它NumPy模块:

>>> import numpy as np

3.3 ndarray:NumPy库的心脏

整个NumPy库的基础是ndarray(N-dimensional array,N维数组)对象。它是一种由同质元素组成的多维数组,元素数量是事先指定好的。同质指的是几乎所有元素的类型和大小都相同。事实上,数据类型由另外一个叫作dtype(data-type,数据类型)的NumPy对象来指定;每个ndarray只有一种dtype类型。

数组的维数和元素数量由数组的(shape)来确定,数组的型由N个正整数组成的元组来指定,元组的每个元素对应每一维的大小。数组的维统称为(axes),轴的数量被称作(rank)。

NumPy数组的另一个特点是大小固定,也就是说,创建数组时一旦指定好大小,就不会再发生改变。这与Python的列表有所不同,列表的大小是可以改变的。

定义ndarray最简单的方式是使用array()函数,以Python列表作为参数,列表的元素即是ndarray的元素。

>>> a = np.array([1, 2, 3])
>>> a
array([1, 2, 3])

检测新创建的对象是否是ndarray很简单,只需要把新声明的变量传递给type()函数即可。

>>> type(a)
<type 'numpy.ndarray'>

调用变量的dtype属性,即可获知新建的ndarray属于哪种数据类型。

>>> a.dtype
dtype('int32')

我们刚建的这个数组只有一个轴,因而秩的数量为1,它的型为(3,1)。这些值的获取方法如下:轴数量需要使用ndim属性,数组长度使用size属性,而数组的型要用shape属性。

>>> a.ndim
1
>>> a.size
3
>>> a.shape
(3L,)

你刚看到的这个数组再简单不过,只有一维。但是数组很容易就能扩展成多维。例如,可以定义一个2×2的二维数组:

>>> b = np.array([[1.3, 2.4],[0.3, 4.1]])
>>> b.dtype
dtype('float64')
>>> b.ndim
2
>>> b.size
4
>>> b.shape
(2L, 2L)

这个数组有两条轴,所以秩为2,每条轴的长度为2

ndarray对象拥有另外一个叫作itemsize的重要属性。它定义了数组中每个元素的长度为几个字节。data属性表示的是包含数组实际元素的缓冲区。该属性至今用得并不多,因为要获取数组中的元素,使用接下来几节即将学到的索引方法即可。

>>> b.itemsize
8
>>> b.data
<read-write buffer for 0x0000000002D34DF0, size 32, offset 0 at 0x0000000002D5FEA0>

3.3.1 创建数组

数组的创建方法有几种,最常用的就是前面你见过的,使用array()函数,参数为单层或嵌套列表。

>>> c = np.array([[1, 2, 3],[4, 5, 6]])
>>> c
array([[1, 2, 3],
       [4, 5, 6]])

除了列表,array()函数还可以接收嵌套元组或元组列表作为参数。

>>> d = np.array(((1, 2, 3),(4, 5, 6)))
>>> d
array([[1, 2, 3],
       [4, 5, 6]])

此外,参数可以是由元组或列表组成的列表,其效果相同。

>>> e = np.array([(1, 2, 3), [4, 5, 6], (7, 8, 9)])
>>> e
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])

3.3.2 数据类型

到目前为止,我们只使用过简单的整型和浮点型数据类型,其实 NumPy数组能够包含多种数据类型(见表3-1)。例如,可以使用字符串类型:

>>> g = np.array([['a', 'b'],['c', 'd']])
>>> g
array([['a', 'b'],
            ['c', 'd']],
      dtype='|S1')
>>> g.dtype
dtype('S1')
>>> g.dtype.name
'string8'

表3-1 NumPy所支持的数据类型

数据类型

说明

bool_

以一个字节形式存储的布尔值(TrueFalse

int_

默认整型(与C中的long相同,通常为int64int32

intc

完全等同于C中的int(通常为int32int64

intp

表示索引的整型(与C中的size_t相同,通常为int32int64

int8

字节(-128~127)

int16

整型(-32768~32767)

int32

整型(-2147483648~2147483647)

int64

整型(-9223372036854775808~9223372036854775807)

uint8

无符号整型(0~255)

uint16

无符号整型(0~65535)

uint32

无符号整型(0~4294967295)

uint64

无符号整型(0~18446744073709551615)

float_

float64的简写形式

float16

半精度浮点型:符号位、5位指数、10位小数部分

float32

单精度浮点型:符号位、8位指数、23位小数部分

float64

双精度浮点型:符号位、11位指数、52位小数部分

complex_

complex128的简写形式

complex64

复数,由两个32位的浮点数来表示(实数部分和虚数部分)

complex128

复数,由两个64位的浮点数来表示(实数部分和虚数部分)

3.3.3 dtype选项

array()函数可以接收多个参数。每个ndarray()对象都有一个与之相关联的dtype对象,该对象唯一定义了数组中每个元素的数据类型。array()函数默认根据列表或元素序列中各元素的数据类型,为ndarray()对象指定最适合的数据类型。但是,你可以用dtype选项作为函数array()的参数,明确指定dtype的类型。

例如,如要定义一个复数数组,可以像下面这样使用dtype选项:

>>> f = np.array([[1, 2, 3],[4, 5, 6]], dtype=complex)
>>> f
array([[ 1.+0.j, 2.+0.j, 3.+0.j],
       [ 4.+0.j, 5.+0.j, 6.+0.j]])

3.3.4 自带的数组创建方法

NumPy库有几个函数能够生成包含初始值的N维数组,数组元素因函数而异。在学习本章乃至全书的过程中,你会发现这些函数非常有用。事实上,有了这些函数,仅用一行代码就能生成大量数据。

例如,zeros()函数能够生成由shape参数指定维度信息、元素均为零的数组。举个例子,下述代码会生成一个3×3型的二维数组:

>>> np.zeros((3, 3))
array([[ 0., 0., 0.],
       [ 0., 0., 0.],
       [ 0., 0., 0.]])

ones()函数与上述函数相似,生成一个各元素均为1的数组。

>>> np.ones((3, 3))
array([[ 1., 1., 1.],
       [ 1., 1., 1.],
       [ 1., 1., 1.]])

这两个函数默认使用float64数据类型创建数组。NumPy arange()函数特别有用。它根据传入的参数,按照特定规则,生成包含一个数值序列的数组。例如,如果要生成一个包含数字0到9的数组,只需传入标识序列结束的数字1作为参数即可。

1用你想得到的序列的最后一个数字再加1作为参数。下面的例子使用了两个参数,其实如上所述,只传入一个参数即可,序列默认从0开始。

>>> np.arange(0, 10)
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

如果不想以0作为起始值,可自己指定,这时需要使用两个参数:第一个为起始值,第二个为结束值。

>>> np.arange(4, 10)
array([4, 5, 6, 7, 8, 9])

还可以生成等间隔的序列。如果为arange()函数指定了第三个参数,它表示序列中相邻两个值之间的差距2有多大。

2也被称作步长。

>>> np.arange(0, 12, 3)
array([0, 3, 6, 9])

此外,第三个参数还可以是浮点型3

3这点就与Python的range()函数有所不同了,range()函数只可以使用整数作为步长。

>>> np.arange(0, 6, 0.6)
array([ 0. , 0.6, 1.2, 1.8, 2.4, 3. , 3.6, 4.2, 4.8, 5.4])

到目前为止,你所创建的都是一维数组。如果要生成二维数组,仍然可以使用arange()函数,但是要结合reshape()函数。后者按照指定的形状,把一维数组拆分为不同的部分。

>>> np.arange(0, 12).reshape(3, 4)
array([[ 0, 1, 2, 3],
       [ 4, 5, 6, 7],
       [ 8, 9, 10, 11]])

另外一个跟arange()函数非常相似的函数是linspace()。它的前两个参数同样是用来指定序列的起始和结尾,但第三个参数不再表示相邻两个数字之间的距离,而是用来指定我们想把由开头和结尾两个数字所指定的范围分成几个部分。

>>> np.linspace(0,10,5)
array([  0. ,   2.5,   5. ,   7.5,   10. ])

最后,来讲讲另外一个创建包含初始值的数组的方法:使用随机数填充数组。可以使用numpy.random模块的random()函数,数组所包含的元素数量由参数指定。

>>> np.random.random(3)
array([ 0.78610272,  0.90630642,  0.80007102])

每次用random()函数生成的数组,其元素均会有所不同。若要生成多维数组,只需把数组的大小作为参数传递给它。

>>> np.random.random((3,3))
array([[ 0.07878569,  0.7176506 ,  0.05662501],
       [ 0.82919021,  0.80349121,  0.30254079],
       [ 0.93347404,  0.65868278,  0.37379618]])

3.4 基本操作

如今你已经知道了新建NumPy数组和定义数组元素的方法。我们该来学习数组的各种运算方法了。

3.4.1 算术运算符

数组的第一类运算是使用算术运算符进行的运算。最显而易见的是为数组加上或乘以一个标量。

>>> a = np.arange(4)
>>> a
array([0, 1, 2, 3])

>>> a+4
array([4, 5, 6, 7])
>>> a*2
array([0, 2, 4, 6])

这些运算符还可以用于两个数组的运算。在NumPy中,这些运算符为元素级。也就是说,它们只用于位置相同的元素之间,所得到的运算结果组成一个新的数组。运算结果在新数组中的位置跟操作数位置相同(见图3-1)。

图3-1 元素级加法

>>> b = np.arange(4,8)
>>> b
array([4, 5, 6, 7])
>>> a + b
array([ 4, 6, 8, 10])
>>> a – b
array([–4, –4, –4, –4])
>>> a * b
array([ 0, 5, 12, 21])

此外,这些运算符还适用于返回值为NumPy数组的函数。例如,你可以用数组a乘上数组b的正弦值或平方根。

>>> a * np.sin(b)
array([–0.       , –0.95892427,   –0.558831 ,   1.9709598 ])
>>> a * np.sqrt(b)
array([ 0.       , 2.23606798,     4.89897949,   7.93725393])

对于多维数组,这些运算符仍然是元素级。

>>> A = np.arange(0, 9).reshape(3, 3)
>>> A
array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])
>>> B = np.ones((3, 3))
>>> B
array([[ 1.,  1.,  1.],
       [ 1.,  1.,  1.],
       [ 1.,  1.,  1.]])
>>> A * B
array([[ 0.,  1.,  2.],
       [ 3.,  4.,  5.],
       [ 6.,  7.,  8.]])

3.4.2 矩阵积

选择使用*号作为元素级运算符是NumPy库比较奇怪的一点。事实上,在很多其他数据分析工具中,*在用于两个矩阵之间的运算时指的是矩阵积(mastrix produet)。而NumPy用dot()函数表示这类乘法,注意,它不是元素级的。

>>> np.dot(A,B)
array([[ 3.,   3.,  3.],
       [ 12., 12., 12.],
       [ 21., 21., 21.]])

所得到的数组中每个元素为,第一个矩阵中与该元素行号相同的元素与第二个矩阵中与该元素列号相同的元素,两两相乘后再求和。图3-2描述的正是矩阵积的计算过程(只给出了矩阵积中两个元素的计算过程)。

图3-2 矩阵积中元素的计算方法

矩阵积的另外一种写法是把dot()函数当作其中一个矩阵对象的方法。

>>> A.dot(B)
array([[ 3.,   3.,  3.],
       [ 12., 12., 12.],
       [ 21., 21., 21.]])

由于矩阵积计算不遵循交换律,因此在这里要多说一句,运算对象的顺序很重要。A*B确实不等于B*A

>>> np.dot(B,A)
array([[ 9.,   12.,   15.],
       [ 9.,   12.,   15.],
       [ 9.,   12.,   15.]])

3.4.3 自增和自减运算符

Python没有++--运算符。对变量的值进行自增与自减,需要使用+=-=运算符。这两个运算符跟前面见过的只有一点不同,运算得到的结果不是赋给一个新数组而是赋给参与运算的数组自身。

>>> a = np.arange(4)
>>> a
array([0, 1, 2, 3])
>>> a += 1
>>> a
array([1, 2, 3, 4])
>>> a –= 1
>>> a
array([0, 1, 2, 3])

因此,这类运算符比每次只能加1的自增运算符用途更广。例如,当你想修改数组元素的值而不想生成新数组时,就可以使用它们。

array([0, 1, 2, 3])
>>> a += 4
>>> a
array([4, 5, 6, 7])
>>> a *= 2
>>> a
array([ 8, 10, 12, 14])

3.4.4 通用函数

通用函数(universal function)通常叫作ufunc,它对数组中的各个元素逐一进行操作。这表明,通用函数分别处理输入数组的每个元素,生成的结果组成一个新的输出数组。输出数组的大小跟输入数组相同。

三角函数等很多数学运算符合通用函数的定义,例如,计算平方根的sqrt()函数、用来取对数的log()函数和求正弦值的sin()函数。

>>> a = np.arange(1, 5)
>>> a
array([1, 2, 3, 4])
>>> np.sqrt(a)
array([ 1.        , 1.41421356,  1.73205081,  2.        ])
>>> np.log(a)
array([ 0.        , 0.69314718,  1.09861229,  1.38629436])
>>> np.sin(a)
array([ 0.84147098, 0.90929743,  0.14112001, –0.7568025 ])

NumPy实现了很多通用函数。

3.4.5 聚合函数

聚合函数是指对一组值(比如一个数组)进行操作,返回一个单一值作为结果的函数。因而,求数组所有元素之和的函数就是聚合函数。ndarray类实现了多个这样的函数。

>>> a = np.array([3.3, 4.5, 1.2, 5.7, 0.3])
>>> a.sum()
15.0
>>> a.min()
0.29999999999999999
>>> a.max()
5.7000000000000002
>>> a.mean()
3.0
>>> a.std()
2.0079840636817816

3.5 索引机制、切片和迭代方法

前几节,我们讲解了数组创建和数组运算。这一节将介绍数组对象的操作方法,以及如何通过索引和切片方法选择元素,以获取数组中某几个元素的视图或者用赋值操作改变元素。最后,我们还会讲解数组的迭代方法。

3.5.1 索引机制

数组索引机制指的是用方括号([])加序号的形式引用单个数组元素,它的用处很多,比如抽取元素,选取数组的几个元素,甚至为其赋一个新值。

新建数组的同时,会生成跟数组大小一致的索引(见图3-3)。

图3-3 一维数组的索引机制

要获取数组的单个元素,指定元素的索引即可。

>>> a = np.arange(10, 16)
>>> a
array([10, 11, 12, 13, 14, 15])
>>> a[4]
14

NumPy数组还可以使用负数作为索引。这些索引同样为递增序列,只不过从0开始,依次增加-1,但实际表示的是从数组的最后一个元素向数组第一个元素移动。在负数索引机制中,数组第一个元素的索引最小。

>>> a[–1]
15
>>> a[–6]
10

方括号内传入多个索引值,可以同时选择多个元素。

>>> a[[1, 3, 4]]
array([11, 13, 14])

再来看下二维数组,它也被称为矩阵。矩阵是由行和列组成的矩形数组,行和列用两条轴来定义,其中轴0用行表示,轴1用列表示。因此,二维数组的索引用一对值来表示:第一个值为行索引,第二个值为列索引。所以,如要获取或选取矩阵中的元素,仍使用方括号,但索引值为两个[行索引,列索引](见图3-4)。

图3-4 二维数组的索引机制

>>> A = np.arange(10, 19).reshape((3, 3))
>>> A
array([[10, 11, 12],
       [13, 14, 15],
       [16, 17, 18]])

因此,如想获取第二行第三列的元素,需要使用索引值[1, 2]。

>>> A[1, 2]
15

3.5.2 切片操作

切片操作是指抽取数组的一部分元素生成新数组。对Python列表进行切片操作得到的数组是原数组的副本,而对NumPy数组进行切片操作得到的数组则是指向相同缓冲区的视图。

如想抽取(或查看)数组的一部分,必须使用切片句法;也就是,把几个用冒号(:)隔开的数字置于方括号里。

如想抽取数组的一部分,例如从第二个到第六个元素这一部分,就需要在方括号里指定起始元素的索引1和结束元素的索引5。

>>> a = np.arange(10, 16)
>>> a
array([10, 11, 12, 13, 14, 15])
>>> a[1:5]
array([11, 12, 13, 14])

如想从上面那一部分元素中,每隔一定数量的元素抽取一个,可以再用一个数字指定所抽取的两个元素之间的间隔大小。例如,间隔为2,表示每隔一个元素抽取一个。

>>> a[1:5:2]
array([11, 13])

为了更好地理解切片句法,你还应该了解不明确指明起始和结束位置的情况。如省去第一个数字,NumPy会认为第一个数字是0(对应数组的第一个元素);如省去第二个数字,NumPy则会认为第二个数字是数组的最大索引值;如省去最后一个数字,它将会被理解为1,也就是抽取所有元素而不再考虑间隔。

>>> a[::2]
array([10, 12, 14])
>>> a[:5:2]
array([10, 12, 14])
>>> a[:5:]
array([10, 11, 12, 13, 14])

对于二维数组,切分句法依然适用,只不过需要分别指定行和列的索引值。例如,只抽取第一行:

>>> A = np.arange(10, 19).reshape((3, 3))
>>> A
array([[10, 11, 12],
       [13, 14, 15],
       [16, 17, 18]])
>>> A[0,:]
array([10, 11, 12])

上面代码中,第二个索引处只使用冒号,而没有指定任意数字,这样选择的是所有列。相反,如果想抽取第一列的所有元素,方括号中的两项应该交换位置。

>>> A[:,0]
array([10, 13, 16])

如要抽取一个小点儿的矩阵,需要明确指定所有的抽取范围。

>>> A[0:2, 0:2]
array([[10, 11],
       [13, 14]])

如要抽取的行或列的索引不连续,可以把这几个索引放到数组中。

>>> A[[0,2], 0:2]
array([[10, 11],
       [16, 17]])

3.5.3 数组迭代

Python数组元素的迭代很简单,只需要使用for结构即可。

>>> for i in a:
...      print i
...
10
11
12
13
14
15

二维数组当然也可以使用for结构,把两个嵌套在一起即可。第一层循环扫描数组的所有行,第二层循环扫描所有的列。实际上,如果遍历矩阵,你就会发现它总是按照第一条轴对矩阵进行扫描。

>>> for row in A:
...      print row
...
[10 11 12]
[13 14 15]
[16 17 18]

如果想遍历矩阵的每个元素,可以使用下面结构,用for循环遍历A.flat

>>> for item in A.flat:
...       print item
...
10
11
12
13
14
15
16
17
18

除了for循环,NumPy还提供另外一种更为优雅的遍历方法。通常用函数处理行、列或单个元素时,需要用到遍历。如果想用聚合函数处理每一列或行,返回一个数值作为结果,最好用纯NumPy方法处理循环:apply_along_axis()函数。

这个函数接收三个参数:聚合函数、对哪条轴应用迭代操作和数组。如果axis选项的值为0,按列进行迭代操作,处理元素;值为1,则按行操作。例如,可以先求每一列的平均数,再求每一行的平均数。

>>> np.apply_along_axis(np.mean, axis=0, arr=A)
array([ 13., 14., 15.])
>>> np.apply_along_axis(np.mean, axis=1, arr=A)
array([ 11., 14., 17.])

上述例子使用了NumPy库定义的函数,但是你也可以自己定义这样的函数。上面还使用了聚合函数,然而,用通用函数也未尝不可。下面的例子,先后按行、列进行迭代操作,但两者的最终结果一致。通用函数apply_along_axis()实际上是按照指定的轴逐元素遍历数组。

>>> def foo(x):
...     return x/2
...
>>> np.apply_along_axis(foo, axis=1, arr=A)
array([[5, 5, 6],
       [6, 7, 7],
       [8, 8, 9]])
>>> np.apply_along_axis(foo, axis=0, arr=A)
array([[5, 5, 6],
       [6, 7, 7],
       [8, 8, 9]])

如上所见,不论是遍历行还是遍历列,通用函数都将输入数组的每个元素做折半处理。

3.6 条件和布尔数组

到目前为止,你已经尝试过用索引和切片方法从数组中选择或抽取一部分元素。这些方法使用数值形式的索引。另外一种从数组中有选择性地抽取元素的方法是使用条件表达式和布尔运算符。

我们来详细看一下这种方法。例如,你想从由0到1之间的随机数组成的4×4型矩阵中选取所有小于0.5的元素。

>>> A = np.random.random((4, 4))
>>> A
array([[ 0.03536295,  0.0035115 ,  0.54742404, 0.68960999],
       [ 0.21264709,  0.17121982,  0.81090212, 0.43408927],
       [ 0.77116263,  0.04523647,  0.84632378, 0.54450749],
       [ 0.86964585,  0.6470581 ,  0.42582897, 0.22286282]])

创建随机数矩阵后,如果使用表示条件的运算符,比如这里的小于号,你将会得到由布尔值组成的数组。对于原数组中条件满足的元素,布尔数组中处于同等位置(也就是小于0.5的元素所处的位置)的元素为True。

>>> A < 0.5
array([[ True,  True, False, False],
       [ True,  True, False,  True],
       [False,  True, False, False],
       [False, False,  True,  True]],  dtype=bool)

实际上,从数组中选取一部分元素时,隐式地用到了布尔数组。其实,直接把条件表达式置于方括号中,也能抽取所有小于0.5的元素,组成一个新数组。

>>> A[A < 0.5]
array([ 0.03536295,  0.0035115 ,  0.21264709,  0.17121982,  0.43408927,
        0.04523647,  0.42582897,  0.22286282])

3.7 形状变换

创建二维数组时,你已经见过用reshape()函数把一维数组转换为矩阵。

>>> a = np.random.random(12)
>>> a
array([ 0.77841574,  0.39654203,  0.38188665,  0.26704305, 0.27519705,
        0.78115866,  0.96019214,  0.59328414,  0.52008642, 0.10862692,
        0.41894881,  0.73581471])
>>> A = a.reshape(3, 4)
>>> A
array([[ 0.77841574,  0.39654203,  0.38188665,  0.26704305],
       [ 0.27519705,  0.78115866,  0.96019214,  0.59328414],
       [ 0.52008642,  0.10862692,  0.41894881,  0.73581471]])

reshape()函数返回一个新数组,因而可用来创建新对象。然而,如果想通过改变数组的形状来改变数组对象,需把表示新形状的元组直接赋给数组的shape属性。

>>> a.shape = (3, 4)
>>> a
array([[ 0.77841574,  0.39654203,  0.38188665,  0.26704305],
       [ 0.27519705,  0.78115866,  0.96019214,  0.59328414],
       [ 0.52008642,  0.10862692,  0.41894881,  0.73581471]])

由输出结果来看,上述操作改变了原始数组的形状,而没有返回新对象。改变数组形状的操作是可逆的,ravel()函数可以把二维数组再变回一维数组。

>>> a = a.ravel()
array([ 0.77841574,  0.39654203,  0.38188665,  0.26704305, 0.27519705,
        0.78115866,  0.96019214,  0.59328414,  0.52008642, 0.10862692,
        0.41894881,  0.73581471])

甚至直接改变数组shape属性的值也可以。

>>> a.shape = (12)
>>> a
array([ 0.77841574,  0.39654203,  0.38188665,  0.26704305,  0.27519705,
        0.78115866,  0.96019214,  0.59328414,  0.52008642,  0.10862692,
        0.41894881,  0.73581471])

另外一种重要的运算是交换行列位置的矩阵转置。NumPy的transpose()函数实现了该功能。

>>> A.transpose()
array([[ 0.77841574, 0.27519705,   0.52008642],
       [ 0.39654203,  0.78115866,  0.10862692],
       [ 0.38188665,  0.96019214,  0.41894881],
       [ 0.26704305,  0.59328414,  0.73581471]])

3.8 数组操作

往往需要用已有数组创建新数组。本节来看一下如何通过连接或切分已有数组创建新数组。

3.8.1 连接数组

你可以把多个数组整合在一起形成一个包含这些数组的新数组。NumPy使用了栈这个概念,提供了几个运用栈概念的函数。例如,vstack()函数执行垂直入栈操作,把第二个数组作为行添加到第一个数组,数组朝垂直方向生长。相反,hstack()函数执行水平入栈操作,也就是说把第二个数组作为列添加到第一个数组。

>>> A = np.ones((3, 3))
>>> B = np.zeros((3, 3))
>>> np.vstack((A, B))
array([[ 1., 1., 1.],
       [ 1., 1., 1.],
       [ 1., 1., 1.],
       [ 0., 0., 0.],
       [ 0., 0., 0.],
       [ 0., 0., 0.]])
>>> np.hstack((A,B))
array([[ 1., 1., 1., 0., 0., 0.],
       [ 1., 1., 1., 0., 0., 0.],
       [ 1., 1., 1., 0., 0., 0.]])

另外两个用于多个数组之间栈操作的函数是column_stack()row_stack()。这两个函数不同于上面两个。一般来讲,这两个函数把一维数组作为列或行压入栈结构,以形成一个新的二维数组。

>>> a = np.array([0, 1, 2])
>>> b = np.array([3, 4, 5])
>>> c = np.array([6, 7, 8])
>>> np.column_stack((a, b, c))
array([[0, 3, 6],
       [1, 4, 7],
       [2, 5, 8]])
>>> np.row_stack((a, b, c))
array([[0, 1, 2],
       [3, 4, 5],
       [6, 7, 8]])

3.8.2 数组切分

上面讲了使用栈操作把多个数组组装到一起的方法。接下来看一下它的逆操作:把一个数组分为几部分。在NumPy中,该操作要用到切分方法。同理,我们也有一组函数,水平切分用hsplit()函数,垂直切分用vsplit()函数。

>>> A = np.arange(16).reshape((4, 4))
>>> A
array([[ 0, 1, 2, 3],
       [ 4, 5, 6, 7],
       [ 8, 9, 10, 11],
       [12, 13, 14, 15]])

水平切分数组的意思是把数组按照宽度切分为两部分,例如4×4矩阵将被切分为两个4×2矩阵。

>>> [B,C] = np.hsplit(A, 2)
>>> B
array([[ 0, 1],
       [ 4, 5],
       [ 8, 9],
       [12, 13]])
>>> C
array([[ 2, 3],
       [ 6, 7],
       [10, 11],
       [14, 15]])

反之,垂直切分指的是把数组按照高度分为两部分,如4×4矩阵将被切为两个2×4矩阵。

>>> [B,C] = np.vsplit(A, 2)
>>> B
array([[0, 1, 2, 3],
       [4, 5, 6, 7]])
>>> C
array([[ 8, 9, 10, 11],
       [12, 13, 14, 15]])

split()函数更为复杂,可以把数组分为几个不对称的部分。此外,除了传入数组作为参数外,还得指定被切分部分的索引。如果指定axis=1项,索引为列索引;如果axis=0,索引为行索引。

例如,要把矩阵切分为三部分,第一部分为第一列,第二部分为第二列、第三列,而第三部分为最后一列。你需要像下面这样指定索引值。

>>> [A1,A2,A3] = np.split(A,[1,3],axis=1)
>>> A1
array([[ 0],
       [ 4],
       [ 8],
       [12]])
>>> A2
array([[ 1, 2],
       [ 5, 6],
       [ 9, 10],
       [13, 14]])
>>> A3
array([[ 3],
       [ 7],
       [11],
       [15]])

你也可以按行切分,方法相同。

>>> [A1,A2,A3] = np.split(A,[1,3],axis=0)
>>> A1
array([[0, 1, 2, 3]])
>>> A2
array([[ 4, 5, 6, 7],
       [ 8, 9, 10, 11]])
>>> A3
array([[12, 13, 14, 15]])

split()函数还具有vsplit()hsplit()函数的功能。

3.9 常用概念

这一节将介绍NumPy库的几个常用概念。我们会讲解副本和视图的区别,其中着重讲解两者返回值的不同点。我们还会介绍NumPy函数的很多事务(transaction)隐式使用的广播机制(broadcasting)。

3.9.1 对象的副本或视图

你可能已经注意到,NumPy中,尤其是在做数组运算或数组操作时,返回结果不是数组的副本就是视图。NumPy中,所有赋值运算不会为数组和数组中的任何元素创建副本。

>>> a = np.array([1, 2, 3, 4])
>>> b = a
>>> b
array([1, 2, 3, 4])
>>> a[2] = 0
>>> b
array([1, 2, 0, 4])

把数组a赋给数组b,实际上不是为a创建副本,b只不过是调用数组a的另外一种方式。事实上,修改a的第三个元素,同样会修改b的第三个元素。数组切片操作返回的对象只是原数组的视图。4

4注意与Python列表切片操作区别开来。列表操作得到的是副本。

>>> c = a[0:2]
>>> c
array([1, 2])
>>> a[0] = 0
>>> c
array([0, 2])

如上所见,即使是切片操作得到的结果,实际上仍指向相同的对象。如果想为原数组生成一份完整的副本,从而得到一个不同的数组,使用copy()函数即可。

>>> a = np.array([1, 2, 3, 4])
>>> c = a.copy()
>>> c
array([1, 2, 3, 4])
>>> a[0] = 0
>>> c
array([1, 2, 3, 4])

上面的例子中,即使改动数组a的元素,数组c仍保持不变。

3.9.2 向量化

向量化和广播这两个概念是NumPy内部实现的基础。有了向量化,编写代码时无需使用显式循环。这些循环实际上不能省略,只不过是在内部实现,被代码中的其他结构代替。向量化的应用使得代码更简洁,可读性更强,你可以说使用了向量化方法的代码看上去更“Pythonic”。向量化使得很多运算看上去更像是数学表达式,例如,NumPy中两个数组相乘可以表示为:

a * b

甚至两个矩阵相乘也可以这么表示:

A * B

其他语言的上述运算要用到多重for结构。例如,计算数组相乘:

for (i = 0; i < rows; i++){
  c[i] = a[i]*b[i];
}

计算矩阵相乘:

for( i=0; i < rows; i++){
   for(j=0; j < columns; j++){
      c[i][j] = a[i][j]*b[i][j];
   }
}

由上可见,使用NumPy时,代码的可读性更强,其表达式更像是数学表达式。

3.9.3 广播机制

广播机制这一操作实现了对两个或以上数组进行运算或用函数处理,即使这些数组形状并不完全相同。并不是所有的维度都要彼此兼容才符合广播机制的要求,但它们必须要满足一定的条件。

前面讲过,在NumPy中,如何通过用表示数组各维度长度的元组(也就是数组的型)把数组转换成多维数组。

因此,若两个数组的各维度兼容,也就是两个数组的每一维等长,或其中一个数组为一维,那么广播机制就适用。如果这两个条件都不能满足,NumPy就会抛出异常,说两个数组不兼容。

>>> A = np.arange(16).reshape(4, 4)
>>> b = np.arange(4)
>>> A
array([[ 0, 1, 2, 3],
       [ 4, 5, 6, 7],
       [ 8, 9, 10, 11],
       [12, 13, 14, 15]])
>>> b
array([0, 1, 2, 3])

执行完上述代码后。我们就得到两个数组:

4 x 4
4

广播机制有两条规则。第一条是为缺失的维度补上个1。如果这时满足兼容性条件,就可以应用广播机制,再来看第二条规则。

4 x 4
4 x 1

兼容性规则满足之后,再来看一下广播机制的第二条规则。这一规则解释的是如何扩展最小的数组,使得它跟最大的数组大小相同,以便使用元素级的函数或运算符。

第二条规则假定缺失元素(一维)都用已有值进行了填充(见图3-5)。

图3-5 应用广播机制的第二条规则

既然两个数组维度相同,它们里面的值就可以相加。

>>> A + b
array([[ 0, 2, 4, 6],
       [ 4, 6, 8, 10],
       [ 8, 10, 12, 14],
       [12, 14, 16, 18]])

上例这种情况比较简单,一个数组较另一个小。还有更复杂的情况,即两个数组形状不同、维度不同、互有长短。

>>> m = np.arange(6).reshape(3, 1, 2)
>>> n = np.arange(6).reshape(3, 2, 1)
>>> m
array([[[0, 1]],

       [[2, 3]],

       [[4, 5]]])
>>> n
array([[[0],
        [1]],

       [[2],
        [3]],

       [[4],
        [5]]])

即使是这种复杂情况,分析两个数组的形状,你也会发现它们相互兼容,因此广播规则仍然适用。

3 x 1 x 2
3 x 2 x 1

这种情况下,两个数组都要扩展维度(进行广播)。

m* = [[[0,1],             n* = [[[0,0],
       [0,1]],                   [1,1]],
      [[2,3],                   [[2,2],
       [2,3]],                   [3,3]],
      [[4,5],                   [[4,4],
       [4,5]]]                   [5,5]]]

然后,就可以对两个数组进行诸如加法这样的元素级运算。

>>> m + n
array([[[ 0,  1],
        [ 1,  2]],

       [[ 4,  5],
        [ 5,  6]],

       [[ 8,  9],
        [ 9, 10]]])

3.10 结构化数组

通过前面几节的多个例子,我们讲了一维数组和二维数组。在NumPy中,不仅可以创建规模更为复杂的数组,还可以创建结构更为复杂的数组,后者叫作结构化数组(structured array),它包含的是结构或记录而不是独立的元素。

例如,你可以创建一个简单的结构化数组,其中元素为结构体。你可以用dtype选项,指定一系列用逗号隔开的说明符,指明组成结构体的元素及它们的数据类型和顺序。

bytes                b1
int                  i1, i2, i4, i8
unsigned ints        u1, u2, u4, u8
floats               f2, f4, f8
complex              c8, c16
fixed length strings a<n>

例如,你想指定由一个整数、一个长度为6的字符串、一个长度为4的float类型和一个长度为8的复数类型组成的结构体,就要在dtype选项中按顺序指定各自的说明符。

>>> structured = np.array([(1, 'First', 0.5, 1+2j),(2, 'Second', 1.3, 2-2j),
(3, 'Third', 0.8, 1+3j)],dtype=('i2, a6, f4, c8'))
>>> structured
array([(1, 'First', 0.5, (1+2j)),
       (2, 'Second', 1.2999999523162842, (2-2j)),
       (3, 'Third', 0.800000011920929, (1+3j))],
      dtype=[('f0', '<i2'), ('f1', 'S6'), ('f2', '<f4'), ('f3', '<c8')])

你还可以在数据类型(dtype)选项中明确指定每个元素的类型,如int8uint8float16complex64等。

>>> structured = np.array([(1, 'First', 0.5, 1+2j),(2, 'Second', 1.3,2-2j),
(3, 'Third', 0.8, 1+3j)],dtype=('
int16, a6, float32, complex64'))
>>> structured
array([(1, 'First', 0.5, (1+2j)),
       (2, 'Second', 1.2999999523162842, (2-2j)),
       (3, 'Third', 0.800000011920929, (1+3j))],
      dtype=[('f0', '<i2'), ('f1', 'S6'), ('f2', '<f4'), ('f3', '<c8')])

然而,上述两种做法结果相同。生成的数组中,dtype序列包含结构体各项的名称及相应的数据类型。

使用索引值,就能获取到包含相应结构体的行。

>>> structured[1]
(2, 'Second', 1.2999999523162842, (2-2j))

自动赋给结构体每个元素的名称可以看成数组列的名称。用它们作为结构化索引,就能引用类型相同或是位于同列的元素。

>>> structured['f1']
array(['First', 'Second', 'Third'],
      dtype='|S6')

如上所见,自动分配的名称的第一个字符为f(field,字段),后面紧跟的是表示它在序列中位置的整数。其实,用更有意义的内容作为名字,用处更大。在创建数组时,可以指定各字段的名称:

>>> structured = np.array([(1,'First',0.5,1+2j),(2,'Second',1.3,2-2j),(3,'Third',0.8,1+3j)],
dtype=[('id','i2'),('position','a6'),('value','f4'),('complex','c8')])
>>> structured
array([(1, 'First', 0.5, (1+2j)),
       (2, 'Second', 1.2999999523162842, (2-2j)),
       (3, 'Third', 0.800000011920929, (1+3j))],
      dtype=[('id', '<i2'), ('position', 'S6'), ('value', '<f4'), ('complex', '<c8')])

或在创建完成后,重新定义结构化数组的dtype属性,在元组中指定各字段的名称。

>>> structured.dtype.names = ('id','order','value','complex')

现在,你可以使用更有意义的字段名来获取数组的某一列。

>>> structured['order']
array(['First', 'Second', 'Third'],
      dtype='|S6')

3.11 数组数据文件的读写

我们还没有讲如何读取文件中的数据。NumPy这方面的内容很重要,用处很大,尤其是在处理数组中包含大量数据的情况时。这在数据分析中很常见,因为要分析的数据集通常都很大,所以由人工来管理这类事务的执行,以及接下来的从一台计算机或计算过程的一段会话读取数据到另一台计算机或另一段会话,是不可取甚至是不可能的。

鉴于此,NumPy提供了几个函数,数据分析师可用其把结果保存到文本或二进制文件中。类似地,NumPy还提供了从文件中读取数据并将其转换为数组的方法。

3.11.1 二进制文件的读写

NumPy的save()方法以二进制格式保存数据,load()方法则从二进制文件中读取数据。

假如你有一个数组要保存,例如数据分析过程产生的结果,调用save()函数即可,参数有两个:要保存到的文件名和要保存的数组,其中文件名中的.npy扩展名系统会自动添加。

>>> data
array([[ 0.86466285,  0.76943895,  0.22678279],
       [ 0.12452825,  0.54751384,  0.06499123],
       [ 0.06216566,  0.85045125,  0.92093862],
       [ 0.58401239,  0.93455057,  0.28972379]])
>>> np.save('saved_data',data)

若要恢复存储在.npy文件中的数据,可以使用load()函数,用文件名作为参数,这次记得添加.npy扩展名。

>>> loaded_data = np.load('saved_data.npy')
>>> loaded_data
array([[ 0.86466285,  0.76943895,  0.22678279],
       [ 0.12452825,  0.54751384,  0.06499123],
       [ 0.06216566,  0.85045125,  0.92093862],
       [ 0.58401239,  0.93455057,  0.28972379]])

3.11.2 读取文件中的列表形式数据

很多时候,你要读写文本格式的数据(如TXT或CSV)。当你使用NumPy或其他应用时,考虑到文本格式的文件不必使用这些应用也能处理,因此一般都会将数据存储为文本格式而不是二进制格式。拿几行CSV(Comma-Separated Values,用逗号分割的值)格式的数据为例。这种格式为列表形式,每两个值之间用逗号隔开(见代码清单3-1)。

代码清单3-1 data.csv

id,value1,value2,value3
1,123,1.4,23
2,110,0.5,18
3,164,2.1,19

NumPy的genfromtxt()函数可以从文本文件中读取数据并将其插入数组中。通常而言,这个函数接收三个参数:存放数据的文件名、用于分割值的字符和是否含有列标题。在接下来这个例子中,分隔符为逗号。

>>> data = np.genfromtxt('data.csv', delimiter=',', names=True)
>>> data
array([(1.0,  123.0, 1.4, 23.0), (2.0, 110.0, 0.5, 18.0),
       (3.0,  164.0, 2.1, 19.0)],
      dtype=[('id', '<f8'), ('value1', '<f8'), ('value2', '<f8'), ('value3', '<f8')])

从输出结果可以看到,我们得到了一个结构化数组,各列的标题变为各字段的名称。

这个函数其实包含两层隐式循环:第一层循环每次读取一行;第二层循环将每一行的多个值分开后,再对这些值进行转化,依次插入所创建的元素。这个函数的优点是它能处理文件中的缺失数据。

以上面的文件为例(见代码清单 3-2),我们从中删除几个元素后,将其另存为data2.csv。

代码清单3-2 data2.csv

id,value1,value2,value3
1,123,1.4,23
2,110,,18
3,,2.1,19

运行下述命令,观察genfromtxt()是怎样把内容为空的项填充为nan值的。

>>> data2 = np.genfromtxt('data2.csv', delimiter=',', names=True)
>>> data2
array([(1.0, 123.0, 1.4, 23.0), (2.0, 110.0, nan, 18.0),
       (3.0, nan, 2.1, 19.0)],
      dtype=[('id', '<f8'), ('value1', '<f8'), ('value2', '<f8'), ('value3', '<f8')])

输出结果中,数组的下面为文件的列标题。可以将这些标题看成能够充当索引的标签,用它们就能按列抽取元素。

>>> data2['id']
array([ 1., 2., 3.])

而按照传统方法,使用数值索引则是按行抽取数据。

>>> data2[0]
(1.0, 123.0, 1.4, 23.0)

3.12 小结

本章介绍了NumPy库所有的主要内容。通过一系列例子,你熟悉了NumPy的多种功能,它们是书中其他内容的基础。事实上,后续多个概念来自其他更为专业的科学计算库,但是这些库的结构参考了NumPy,并且是以NumPy库为基础进行开发的。

你还从本章学到了ndarray如何扩展了Python的功能,从而使其适用于科学计算,尤其是数据分析。

对任何想从事数据分析的人来说,NumPy都是一项至关重要的技能。

下一章,我们将介绍一个新库pandas。它以NumPy为基础,吸收了本章讲到的所有基础概念,并对它们进行了扩展,以使其更适合数据分析。

目录

  • 版权声明
  • 致谢
  • 译者序
  • 第 1 章 数据分析简介
  • 第 2 章 Python世界简介
  • 第 3 章 NumPy库
  • 第 4 章 pandas库简介
  • 第 5 章 pandas:数据读写
  • 第 6 章 深入pandas:数据处理
  • 第 7 章 用matplotlib实现数据可视化
  • 第 8 章 用scikit-learn库实现机器学习
  • 第 9 章 数据分析实例——气象数据
  • 第 10 章 IPython Notebook内嵌JavaScript库D3
  • 第 11 章 识别手写体数字
  • 附录 A 用LaTeX编写数学表达式
  • 附录 B 开放数据源