一、切片和索引
- ndarray 对象的内容可以通过索引或切片来访问和修改(),与 Python 中 list 的切片操作一样。
- ndarray 数组可以基于 0 - n 的下标进行索引(先行后列,都是从 0 开始)。
- 区别在于:数组切片是原始数组视图(这就意味着,如果做任何修改,原始都会跟着更改)。 这也意味着,如果不想更改原始数组,我们需要进行显式的复制,从而得到它的副本(.copy())。
- 冒号分隔切片参数
[start:stop:step]
。 - 在开始之前,先导入numpy包
import numpy as np
1. 一维数组
- 通过 numpy.arange() 生成一个等区间的数组(起始值默认为 0,步长默认为 1,终止值设置为 10)。
ar1 = np.arange(10)
ar1
#array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
- 从索引 2 开始到索引 7 停止,间隔为 2。
ar2 = ar1[2:7:2]
ar2
#array([2, 4, 6])
- 这里需要解释以下
冒号
的作用: - 如果只放置一个参数,如果是 [2],将返回与该索引相对应的单个元素;如果为 [2:],表示从该索引开始以后的所有项都将被提取;如果为 [:2],表示不限制开始元素,但会在该元素截止。
- 如果使用了两个参数,如 [2:7],那么则提取两个索引(不包括停止索引)之间的项。
- 返回索引对应的数据(注意是从0开始的)。
print('ar1:',ar1)
print('ar1[4]:',ar1[4])
#ar1: [0 1 2 3 4 5 6 7 8 9]
#ar1[4]: 4
- 通过 numpy.arange() 生成一个等区间的数组(起始值设置为 1,终止值设置为 20,步长设置为 2)。
ar3 = np.arange(1,20,2)
ar3
#array([ 1, 3, 5, 7, 9, 11, 13, 15, 17, 19])
-
那么,我们如何取出元素 9?
-
由于索引是从 0 开始的,因此,我们输出 ar3[4] 即可。
-
(1) 从该索引开始以后的所有项都将被提取
-
从索引 2 开始,提取出后面所有数据。
print(ar3)
ar3[2:]
#[ 1 3 5 7 9 11 13 15 17 19]
#array([ 5, 7, 9, 11, 13, 15, 17, 19])
- (2) 从索引开始,到索引结束(不包含结束)
- 从索引 2 开始到索引 7(包含索引 7)。
print(ar3)
ar3[2:7]
ar3[:-2]
#[ 1 3 5 7 9 11 13 15 17 19]
#array([ 1, 3, 5, 7, 9, 11, 13, 15])
- (3) 步长
- 取所有数据,步长为 -1(负数代表从右往左数)。
ar3[::-1]
#array([19, 17, 15, 13, 11, 9, 7, 5, 3, 1])
- 取数据 1 到 17 之间, 并且间隔一位的数据。
ar3[1:-2:2]
print(ar3)
print(ar3[:6])
print(ar3[6:])
#[ 1 3 5 7 9 11 13 15 17 19]
#[ 1 3 5 7 9 11]
#[13 15 17 19]
- (4) 为什么切片和区间会忽略最后一个元素
- 计算机科学家 edsger w.dijkstra(艾兹格·W·迪科斯彻),delattr 这一风格的解释应该是比较好的:
A、当只有最后一个位置信息时,我们可以快速看出切片和区间里有几个元素:range(3) 和 my_list[:3]。
B、当起始位置信息都可见时,我们可以快速计算出切片和区间的长度,用有一个数减去第一个下表(stop-start)即可。
C、这样做也让我们可以利用任意一个下标把序列分割成不重叠的两部分,只要写成 my_list[:x] 和 my_list[x:] 就可以了。
2.二维数组
- 同样适用上述索引提取方法:
- 我们定义 4 行 5 列的数据。
- np.arange(20) 表示起始值默认为 0,终止值设置为 20,步长默认为 1。
- reshape(4,5) 表示生成一个 4 行 5 列的二维数组。
ar4_5 = np.arange(20).reshape(4,5)
ar4_5
#array([[ 0, 1, 2, 3, 4],
# [ 5, 6, 7, 8, 9],
# [10, 11, 12, 13, 14],
# [15, 16, 17, 18, 19]])
- 我们通过 ar4_5.ndim 返回 ar4_5 的秩(也就是几维)
ar4_5.ndim
#2
- 切片表示的是下一维度的一个元素,所以其代表一维数组
ar4_5[2]
#array([10, 11, 12, 13, 14])
- 我们也可以通过二次索引取得一维数组当中的元素
ar4_5[2][2]
#12
- 打印出 ar4_5 整体数组,便于后续的操作观察
print(ar4_5)
#[[ 0 1 2 3 4]
# [ 5 6 7 8 9]
# [10 11 12 13 14]
# [15 16 17 18 19]]
- 我们可以取出从第三行和第四行的元素。
ar4_5[2:]
#array([[10, 11, 12, 13, 14],
# [15, 16, 17, 18, 19]])
- 注意:切片还可以使用省略号 “…”,如果在行位置使用省略号,那么返回值将包含所有行元素,反之,则包含所有列元素。
- 例如,我们需要取得第二列数据(由于索引是从 0 开始的,因此 第二列的元素就是 1)。
- 取出的就是每一行和第二列交叉的数据,也就是 1,6,11,16。
ar4_5[...,1]
#array([ 1, 6, 11, 16])
- 我们需要返回第二列后的所有项。
ar4_5[...,1:]
#array([[ 1, 2, 3, 4],
# [ 6, 7, 8, 9],
# [11, 12, 13, 14],
# [16, 17, 18, 19]])
- 我们需要返回第 2 行第 3 列的数据(对于取出单个数值,两种方法是一样的)。
ar4_5[1,2] # 对应(1,2) 的元素
#7
- 对于返回第 2 行第 3 列的数据,我们也可以使用如下方法。
ar4_5[1][2]
#7
- 对于上述关于取出单个数据的方法,是否可以映射到取出一列元素呢?
- 如下例子所示,答案是否定的。
- 首先,我们执行第一步操作,就是 ar4_5[…]。此时,代表取出 ar4_5 的每一行元素,也就是打印 ar4_5 本身。
- 然后,我们执行第二步操作 ar4_5[…][1] ,就是切片,在 ar4_5 本身上打印处第二行,也就是 [5, 6, 7, 8, 9]。
ar4_5[...][1]
array([5, 6, 7, 8, 9])
二、索引的高级操作
在 NumPy 中还可以使用高级索引方式,比如整数数组索引、布尔索引,以下将对两种索引方式做详细介绍。
1. 整数数组索引
- 我们先创建一个二维数组
x = np.array([
[1,2],
[3,4],
[5,6]
])
y = x[[0,1,2],[0,1,0]]
y
#array([1,4,5])
- 其中,[0,1,2] 代表行索引,[0,1,0] 代表列索引,表示 y 分别获取 x 中的 (0,0)、(1,1) 和 (2,0) 表示的数据。
- 然后,我们先创建一个 43 的数组,创建完成之后,获取了 4*3 数组中的四个角上元素,它们对应的行索引是 [0,0] 和 [3,3],列索引是 [0,2] 和 [0,2]。
b = np.array([[ 0, 1, 2],
[ 3, 4, 5],
[ 6, 7, 8],
[ 9,10,11]])
a = b[[0,0,3,3],[0,2,0,2]] # (0,0) (0,2) (3,0) (3,2)
r = np.array([[0,0] ,[3,3]]).reshape(4) #用r代替[0,0,3,3]
l = np.array([[0,2] ,[0,2]]).reshape(4) #用l代替[0,2,0,2]
s = b[r,l].reshape((2,2))
s
#array([[ 0, 2],
# [ 9, 11]])
- 然后,我们先创建一个 3*3 的数组,创建完成之后,行取出第 2 行和第 3 行的元素,列取出第 2 列和第 3 列的数据。
- 这里需要注意的是,不包含终止索引值。
a = np.array([
[1,2,3],
[4,5,6],
[7,8,9]
])
b = a[1:3, 1:3]
b
#array([[5, 6],
# [8, 9]])
- 也就是 1:3 == [1,2]。
- 高级索引操作与常规的索引操作相同,都可以使用 … 表示所有行,1: 表示从第二列开始的所有列。
c = a[1:3,[1,2]]
d = a[...,1:]
print(b)
print(c)
print(d)
#[[5 6]
# [8 9]]
#[[5 6]
# [8 9]]
#[[2 3]
# [5 6]
# [8 9]]
2. 布尔数组索引
- 当输出的结果需要经过布尔运算(如比较运算)时,此时会使用到另一种高级索引方式,即布尔数组索引。
- 下面示例返回数组中大于 6 的的所有元素:
x = np.array([[ 0, 1, 2],[ 3, 4, 5],[ 6, 7, 8],[ 9, 10, 11]])
print(x.shape)
x[x>6]
#(4, 3)
#array([ 7, 8, 9, 10, 11])
- 布尔索引所实现的是通过一维数组中的每个元素的布尔型数值对一个与一维数组有着同样行数或列数的矩阵进行符合匹配。
- 这种作用,其实是把一维数组中布尔值为 True 的相应行或列给抽取了出来。
- 这里需要注意的是,一维数组的长度必须和想要切片的维度或轴的长度一致。
练习
(1) 提取出数组中所有奇数。
x = np.array([[ 0, 1, 2],[ 3, 4, 5],[ 6, 7, 8],[ 9, 10, 11]])
x[x%2 == 1]
#array([ 1, 3, 5, 7, 9, 11])
(2) 将奇数值修改为 -1。
x = np.array([[ 0, 1, 2],[ 3, 4, 5],[ 6, 7, 8],[ 9, 10, 11]])
x[x%2 == 1] = -1
x
#array([[ 0, -1, 2],
# [-1, 4, -1],
# [ 6, -1, 8],
# [-1, 10, -1]])
筛选出指定区间内数据
- & 表示和。
- | 表示或。
- 将数组 x 中大于 4 并且小于 9 的数据提取出来。
x = np.array([[ 0, 1, 2],[ 3, 4, 5],[ 6, 7, 8],[ 9, 10, 11]])
x[(x>4) & (x<9)]
#array([5, 6, 7, 8])
- True 和 False 的形式表示需要和不需要的数据
- 我们先创建 3*4 的数组,元素通过 np.arange(12) 自动生成,表示从起始值 0 到终止值(不包括终止值)12,步长是 1。
a3_4 = np.arange(12).reshape((3,4))
a3_4
#array([[ 0, 1, 2, 3],
# [ 4, 5, 6, 7],
# [ 8, 9, 10, 11]])
- 将行变量和列变量分别用 True 和 False 表示,其中行变量和列变量按如下代码设置。
row1 = np.array([False,True,True])
column1 = np.array([True,False,True,False])
- a3_4 是 3 行, 做切片时也提供 3 个元素的数组,轴的长度一致。
- 通过 row1 和 column1 中的 True 和 False 就可以输出我们需要的元素,True 表示需要,False 表示不需要。
a3_4[row1]
#array([[ 4, 5, 6, 7],
# [ 8, 9, 10, 11]])
a3_4[:,column1]
#array([[ 0, 2],
# [ 4, 6],
# [ 8, 10]])
- 那么我们是否可以用两个一维布尔数组进行切片呢?我们继续进行试验得到如下结果:
a3_4[row1,column1]
#array([ 4, 10])
从结果上看,它实际上等价于下面的代码。
a3_4[[1, 2],[0, 2]]
- 输出的相当于是坐标序号为 (0,1),(2,2) 的数。
- 索引形状不匹配
- 从上面的结果可以知道,能够进行切片的前提是:两个一维布尔数组中 True 的个数需要相等,否则会出现下面的情况。
- 行变量当中存在 2 个 True 元素,列变量当中存在 3 个 True 元素。
row2 = np.array([False,True,True])
column2 = np.array([True,False,True,True])
- 其中,行变量和列变量当中的 True 元素个数不一致,会导致错误。
- a3_4[row2, column2] 就相当于 a3_4[[1,2],[0,2,3]]。
- 报错信息如下:indexError: shape mismatch: indexing arrays could not be broadcast together with shapes (2,) (3,)。
- 是因为所选索引形状不匹配:索引数组无法与该形状一起广播。
- 当访问 numpy 多维数组时,用于索引的数组需要具有相同的形状(行和列)。numpy 将有可能去广播,所以,若要为了实现不同维度的选择, 可分两步对数组进行选择。
(1) 例如我需要选择第 1 行和最后一行的第 1,3,4 列时,先选择行,再选择列。
(2) 先读取数组 a3_4 的第一行和最后一行,保存到 temp ,然后再筛选相应的列即可。
a3_4
#array([[ 0, 1, 2, 3],
# [ 4, 5, 6, 7],
# [ 8, 9, 10, 11]])
- 那么,我们需要选择第 1 行和最后一行的第 1,3,4 列元素,操作步骤如下:
(1) 先选行。
temp = a3_4[[0,-1],:]
temp
#array([[ 0, 1, 2, 3],
# [ 8, 9, 10, 11]])
(2) 再选择列。
temp[:,[0,2,3]]
temp
#array([[ 0, 2, 3],
# [ 8, 10, 11]])
(3) 合并一条。
a3_4[[0,-1],:][:,[0,2,3]]
- 但是,如果有一个一维数组长度是 1 时,这时又不会报错:
- 比如说,如果我们需要提取第二行,第 1,3 列的数据。
a3_4[[1],[0,2]]
#array([4, 6])
- 以上相当于 (1,0) 和 (1,2)。
- 数组索引及切片的值更改会修改原数组
- 一个标量赋值给一个索引/切片时,会自动改变/传播原始数组。
ar = np.arange(10)
print(ar)
ab = ar[:]
ar[5] = 100
print(ab)
#[0 1 2 3 4 5 6 7 8 9]
#[ 0 1 2 3 4 100 6 7 8 9]
- 可以使用复制操作
ar = np.arange(10)
b = ar.copy()
- 或者可以使用 b = np.array(ar)。
b[7:9] = 200
print('ar:',ar)
print('b:',b)
三、广播机制
- 广播(Broadcast)是 numpy 对不同形状(shape)的数组进行数值计算的方式, 对数组的算术运算通常在相应的元素上进行。
- 如果两个数组 a 和 b 形状相同,即满足 a.shape == b.shape,那么 a*b 的结果就是 a 与 b 数组对应位相乘。这要求维数相同,且各维度的长度相同。
a = np.array([1,2,3,4])
b = np.array([10,20,30,40])
c = a * b
print (c)
#[ 10 40 90 160]
- 但如果两个形状不同的数组呢?它们之间就不能做算术运算了吗?
- 当然不是!为了保持数组形状相同,NumPy 设计了一种广播机制, 这种机制的核心是对形状较小的数组,在横向或纵向上进行一定次数的重复,使其与形状较大的数组拥有相同的维度。
a = np.array([[ 0, 0, 0],
[10,10,10],
[20,20,20],
[30,30,30]])
b = np.array([1,2,3])
print(a + b)
- 4x3 的二维数组与长为 3 的一维数组相加,等效于把数组 b 在二维上重复 4 次再运算
1. 广播机制规则
广播具有如下规则:
(1) 让所有输入数组都向其中形状最长的数组看齐,形状中不足的部分都通过在前面加 1 补齐。
(2) 输出数组的形状是输入数组形状的各个维度上的最大值。
(3) 如果输入数组的某个维度和输出数组的对应维度的长度相同或者其长度为 1 时,这个数组能够用来计算,否则出错。
(4) 当输入数组的某个维度的长度为 1 时,沿着此维度运算时都用此维度上的第一组值。
为了更清楚的理解这些规则看,来看几个具体的示例
- 我们通过 np.ones() 生成一个全 1 数组,np.arange() 生成一个有规律元素的数组。
- 两个数组的形状分别为 M.shape=(2,3),a.shape=(3,)。
- 可以看到,根据规则 1,数组 a 的维度更小,所以在其左边补 1,变为 M.shape -> (2,3), a.shape -> (1,3)。
- 根据规则 2,第一个维度不匹配,因此扩展这个维度以匹配数组:M.shape ->(2,3), a.shape -> (1,3)。
- 现在两个数组的形状匹配了,可以看到它们的最终形状都为(2,3)。
M = np.ones((2,3))
print(M)
a = np.arange(3)
print(a)
M + a
#[[1. 1. 1.]
# [1. 1. 1.]]
#[0 1 2]
#array([[1., 2., 3.],
# [1., 2., 3.]])
- 我们通过 np.arange() 生成两个有规律元素数组,其中一个设置为三行一列,便于对广播机制进行验证。
- 两个数组的形状设置为 a.shape=(3,1), b.shape=(3,)。
- 规则 1 告诉我们,需要用 1 将 b 的形状补全:a.shape -> (3,1), b.shape -> (1,3)。
- 规则 2 告诉我们,需要更新这两个数组的维度来相互匹配:a.shape -> (3,3), b.shape -> (3,3)
- 因为结果匹配,所以这两个形状是兼容的。
a = np.arange(3).reshape((3,1))
print('a:',a)
b = np.arange(3)
print('b:',b)
a + b
#a: [[0]
# [1]
# [2]]
#b: [0 1 2]
#array([[0, 1, 2],
# [1, 2, 3],
# [2, 3, 4]])
2. 对于广播规则另一种简单理解
(1) 将两个数组的维度大小右对齐,然后比较对应维度上的数值。
(2) 如果数值相等或其中有一个为1或者为空,则能进行广播运算。
(3) 输出的维度大小为取数值大的数值。否则不能进行数组运算。
- 数组 a 大小为(2, 3),数组 b 大小为 (1,),对其进行右对齐操作。
2 3
1
----------
2 3
- 所以最后两个数组运算的输出大小为:(2, 3),下面对其进行代码验证。
a = np.arange(6).reshape(2, 3)
print('a:', a)
b = np.array([5])
print('b:',b)
c = a * b
c
#a: [[0 1 2]
# [3 4 5]]
#b: [5]
#array([[ 0, 5, 10],
# [15, 20, 25]])
- 数组 a 大小为 (2, 1, 3),数组 b 大小为 (4, 1),对其进行右对齐操作。
2 1 3
4 1
----------
2 4 3
- 所以最后两个数组运算的输出大小为:(2, 4, 3)。
a= np.arange(6).reshape(2, 1, 3)
print('a:',a)
print('-'*10)
b = np.arange(4).reshape(4, 1)
print('b:',b)
print('-'*10)
c = a + b
print(c,c.shape)
#a: [[[0 1 2]]
#
# [[3 4 5]]]
#----------
#b: [[0]
# [1]
# [2]
# [3]]
#----------
#[[[0 1 2]
# [1 2 3]
# [2 3 4]
# [3 4 5]]
#
# [[3 4 5]
# [4 5 6]
# [5 6 7]
# [6 7 8]]] (2, 4, 3)
- 从这里能够看出:
(1) 两个数组右对齐以后,对应维度里的数值要么相等,要么有一个为 1,要么缺失取大值。
(2) 除此之外就会报错。像下面的两个数组就不能做运算。 - 数组 a 大小为 (2, 1, 3),数组 b 大小为 (4, 2),对其进行右对齐操作。
2 1 3
4 2
----------
- 2 跟 3 不匹配,此时就不能做运算。
a = np.arange(6).reshape(2, 1, 3)
print('a:',a)
print('-'*10)
b = np.arange(8).reshape(4, 2)
print('b:',b)
print('-'*10)
a + b
#a: [[[0 1 2]]
#
# [[3 4 5]]]
#----------
#b: [[0 1]
# [2 3]
# [4 5]
# [6 7]]
#----------
#---------------------------------------------------------------------------
#ValueError Traceback (most recent call last)
#<ipython-input-68-2ebfaa0877fb> in <module>
# 9 print('-'*10)
# 10 # 运行报错
#---> 11 a + b
#
#ValueError: operands could not be broadcast together with shapes (2,1,3) (4,2)
- 广播机制能够处理不同大小的数组,数组之间必须满足广播的规则,否则就会报错。
- 事实上,相同大小的数组运算也遵循广播机制。