NumPy库
NumPy是用Python进行科学计算,尤其是数据分析时,所用到的一个基础库。它是大量Python数学和科学计算包的基础。
5 索引、切片和迭代方法
5.1 索引机制
数组索引机制指的是用方括号([ ])加序号的形式引用单个数组元素,它的用处很多,比如抽取元素,选取数组的几个元素,甚至为其赋一个新值。
新建数组的同时,会生成跟数组大小一致的索引。
要获取数组的单个元素,指定元素的索引即可。
>>> 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用列表示。因此,二维数组的索引用一对值来表示:第一个值为行索引,第二个值为列索引。所以,如要获取或选取矩阵中的元素,仍使用方括号,但索引值为两个[行索引,列索引]。
>>> 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
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]])
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]])
如上所见,不论是遍历行还是遍历列,通用函数都将输入数组的每个元素做折半处理。
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])
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]])
8 数组操作
通过连接或切分已有数组创建新数组。
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]])
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()函数的功能。