1.基本索引和切片
基本索引和切片是 NumPy 中访问数组元素的最常见方式,类似于 Python 列表的索引和切片,但功能更强大,尤其是在多维数组中。
(1)一维数组
对于一维数组,基本索引和切片与 Python 列表非常相似:
- 索引单个元素: 使用方括号
[]
和一个整数索引来访问单个元素。索引从0
开始。负数索引表示从数组末尾开始计数(-1
是最后一个元素)。arr[i]
:访问索引为i
的元素。arr[-i]
:访问倒数第i
个元素。
- 切片(Slicing): 使用
[start:stop:step]
语法来获取数组的一个子序列。start
:切片的起始索引(包含)。如果省略,默认为0
。stop
:切片的结束索引(不包含)。如果省略,默认为数组的长度。step
:步长,表示每隔多少个元素取一个。如果省略,默认为1
。arr[start:stop]
:从start
到stop-1
的元素。arr[start:]
:从start
到末尾的元素。arr[:stop]
:从开头到stop-1
的元素。arr[::step]
:以指定步长获取所有元素。arr[::-1]
:反转数组。arr[::-step]
:反转数组并以指定步长获取。
(2)多维数组
对于多维数组,索引和切片扩展到每个维度。可以使用逗号 ,
分隔每个维度的索引或切片。
- 索引单个元素:
arr2d[row_index, col_index]
:访问二维数组中特定行和列的元素。这等价于arr2d[row_index][col_index]
,但前者更高效和推荐。- 对于更高维数组,以此类推:
arr3d[dim1_idx, dim2_idx, dim3_idx]
。
- 切片多维数组:
arr2d[row_slice, col_slice]
:同时对行和列进行切片。:
表示选择该维度的所有元素。- 例如,
arr2d[:2, 1:]
表示选择前两行(索引 0 和 1),以及从第二列(索引 1)到最后一列的所有列。
1
2
3
4
5
6
7
8
9
10
11
12
arr2d = np.arange(1, 10).reshape(3, 3)
print(arr2d)
# 第一行最后一列
print(arr2d[0, -1])
# 第三行第二列
print(arr2d[2, 1])
# 前两行后2列
print(arr2d[:2, 1:])
# 从第二行到末尾,前2列
print(arr2d[1:, :2])
# 所有行,第一列
print(arr2d[:, 0])
(3)切片是视图
视图是浅拷贝的意思。
这是 NumPy 中一个非常重要的概念:NumPy 中的数组切片是原始数组的视图(view),而不是副本(copy)。 这意味着:
- 切片操作不会复制数据,而是创建一个新的数组对象,这个新对象共享原始数组的底层数据内存。
- 对切片(视图)的任何修改都会直接反映到原始数组上,因为它们操作的是同一块数据。
- 这与 Python 列表的切片行为不同,Python 列表的切片会创建新的列表副本。
- 这种设计是为了提高性能和内存效率,尤其是在处理大型数据集时。
切片赋值:您可以使用切片来修改数组的多个元素。当您对一个切片进行赋值时,NumPy 会将赋值的值广播(broadcast)到切片所对应的原始数组的元素上。
NumPy 数组的底层数据存储在连续的内存块中。数组对象本身包含元数据(如形状
shape
、数据类型dtype
、步长strides
和指向数据缓冲区的指针data
)。
- 索引: 当您使用整数索引时,NumPy 会根据数组的
strides
(每跨越一个元素在内存中需要跳过的字节数)和offset
(起始偏移量)来计算出目标元素在数据缓冲区中的精确内存地址,然后直接访问该地址的数据。- 切片: 当您创建切片时,NumPy 不会分配新的数据缓冲区。相反,它会创建一个新的数组对象。这个新数组对象:
- 拥有自己的
shape
和strides
,这些shape
和strides
是根据切片规则计算出来的,以正确地“视图”原始数据。
- 它的数据指针
data
仍然指向原始数组的数据缓冲区,但可能有一个新的起始偏移量。- 它的
base
属性会指向原始数组对象(如果它是视图)。- 它的
flags.owndata
属性会是False
。这种“视图”机制使得切片操作非常快速,因为它避免了昂贵的数据复制。
(4)选择题
-
给定以下 NumPy 代码:
1 2 3 4
import numpy as np a = np.array([10, 20, 30, 40, 50]) b = a[1:4] b[0] = 99
执行上述代码后,
a
数组的值是什么?A.
[10, 20, 30, 40, 50]
B.[10, 99, 30, 40, 50]
C.[10, 99, 99, 99, 50]
D.[99, 20, 30, 40, 50]
答案:B
b = a[1:4]
创建了一个视图,b
引用了a
的[20, 30, 40]
部分。b[0] = 99
修改的是b
的第一个元素,也就是a
的第二个元素(20
变成了99
)。 -
对于二维 NumPy 数组
arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
,表达式arr2d[1:, :2]
的结果是什么? A.[[4, 5], [7, 8]]
B.[[2, 3], [5, 6], [8, 9]]
C.[[1, 2], [4, 5]]
D.[[4, 5, 6], [7, 8, 9]]
答案:A
arr2d[1:, :2]
意味着:- 行:从索引
1
开始到末尾(即第 2 行和第 3 行)。 - 列:从索引
0
到2-1=1
(即第 1 列和第 2 列)。
所以它选择了
arr2d
的[1,0], [1,1]
和[2,0], [2,1]
对应的元素。 - 行:从索引
(5)编程题
创建一个 5*5
的 NumPy 数组,其中所有元素都是 1 到 25 的整数(例如,使用 np.arange(1, 26).reshape(5, 5)
)。
使用基本切片操作:
- 提取数组的中心
3*3
子数组。 - 提取数组的偶数行和奇数列。
- 将数组的第三行所有元素设置为 0。
打印每次操作后的数组。
1
2
3
4
5
6
7
8
9
10
11
12
arr = np.arange(1, 26).reshape(5, 5)
print(arr)
arr_center = arr[1:4, 1:4]
print(arr_center)
arr2 = arr[::2, [1,3]]
print(arr2)
# 或者
arr2_ = arr[::2, 1::2]
print(arr2_)
arr3 = arr.copy()
arr3[2] = 0
print(arr3)
2.花式索引和索引技巧
花式索引(Fancy Indexing)是 NumPy 中一种强大的索引方式,它允许使用整数数组或布尔数组作为索引来选择非连续的元素。与基本切片不同,花式索引总是将数据复制到新数组中,这意味着它返回的是一个副本,而不是视图。
(1)整数数组索引
当使用一个整数数组(或列表)作为索引时,NumPy 会根据这些索引值来选择对应的元素或行/列。
- 一维数组:
arr[[idx1, idx2, ...]]
:返回一个新数组,其中包含idx1
,idx2
等索引位置的元素。- 索引数组中的值可以重复,这将导致结果数组中出现重复的元素。
- 结果数组的形状与索引数组的形状相同。
- 多维数组:
- 选择多行/多列: 当只对一个维度使用整数数组索引时,它会选择对应的行(对于第一维)或列(对于第二维)。
arr2d[[row_idx1, row_idx2]]
:选择指定行。arr2d[:, [col_idx1, col_idx2]]
:选择指定列。
- 选择特定位置的元素(配对索引): 当您为每个维度都提供一个相同长度的整数数组作为索引时,NumPy 会将这些索引数组中的对应元素配对,从而选择特定位置的元素。
arr2d[[row_indices], [col_indices]]
:例如,arr2d[[1, 3], [2, 4]]
会选择arr2d[1, 2]
和arr2d[3, 4]
两个元素。结果是一个一维数组。
- 选择多行/多列: 当只对一个维度使用整数数组索引时,它会选择对应的行(对于第一维)或列(对于第二维)。
【1】一维数组花式索引
1
2
3
4
5
6
7
8
arr = np.arange(1, 11)
print(arr)
arr1 = arr[[1, 1, 3, 3, 7, 8, 9]]
print(arr1)
# 花式索引返回的是副本,不是视图
print(arr1.base) # None
print(arr1.flags.owndata) # True
【2】二维数组花式索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
"""
[[41 97 39 81 85]
[ 3 73 26 8 45]
[ 1 69 12 48 18]
[92 21 31 30 46]]
[[ 3 73 26 8 45]
[92 21 31 30 46]]
[73 46]
"""
arr = np.random.randint(1, 100, (4, 5))
print(arr)
# 选择第2行和第四行
arr1 = arr[[1, 3]]
print(arr1)
# 选择第二行第二个,和 最后一行最后一个
arr3 = arr[[1, 3], [1, 4]]
print(arr3)
np.ix_()
函数
np.ix_()
函数是一个非常有用的工具,用于组合不同的向量(一维索引数组)来选择一个区域(网格)。它返回一个元组,其中包含用于构建 N 维网格索引的 N 个数组。
np.ix_([row_indices], [col_indices])
:这会生成两个数组,当它们用于索引二维数组时,会选择由row_indices
和col_indices
定义的所有交叉点。- 结果等价于先用行索引进行花式索引,再对结果用列索引进行花式索引(或反之)。例如,
arr2d[np.ix_([1,3],[2,4])]
等价于arr2d[[1,3]][:,[2,4]]
。 np.ix_
的主要优势在于它能够处理任意维度的交叉索引,并且在内部优化了索引数组的创建。
花式索引的工作原理与基本切片不同,因为它需要从原始数据中提取非连续的元素,并将它们组织成一个新的数组。
- 数据复制: 当您使用整数数组进行花式索引时,NumPy 会遍历索引数组中的每一个索引值。对于每个索引,它会计算出原始数组中对应元素的内存地址,然后将该元素的值复制到一个新分配的内存区域。
- 新数组创建: 最终,所有被选择的元素都被复制到一个全新的 NumPy 数组对象中。这个新数组拥有自己的数据缓冲区,因此对它的修改不会影响到原始数组。
base
和flags.owndata
: 花式索引返回的数组的base
属性将是None
,并且flags.owndata
将是True
。- 性能: 由于涉及数据复制,花式索引通常比基本切片(视图)的性能开销更大,尤其是在处理大型数组时。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
print(arr)
"""
[[41 97 39 81 85]
[ 3 73 26 8 45]
[ 1 69 12 48 18]
[92 21 31 30 46]]
"""
row_index = [1, 3, 3, 3]
col_index = [2, 4, 4]
selected_data = arr[np.ix_(row_index, col_index)]
print(selected_data)
"""
[[26 45 45]
[31 46 46]
[31 46 46]
[31 46 46]]
"""
(2)选择题
-
给定以下 NumPy 代码:
1 2 3 4
import numpy as np data = np.array([10, 20, 30, 40, 50]) result = data[[0, 2, 4]] result[0] = 100
执行上述代码后,
data
数组的值是什么?A.
[100, 20, 30, 40, 50]
B.[10, 20, 30, 40, 50]
C.[100, 20, 100, 40, 100]
D.[10, 20, 100, 40, 50]
答案:B ,花式索引
data[[0, 2, 4]]
返回的是data
的一个副本。对
result
的修改不会影响到原始data
数组。 -
对于二维 NumPy 数组
matrix = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
,表达式matrix[[0, 2], [1, 0]]
的结果是什么? A.[[2, 3], [7, 8]]
B.[2, 7]
C.[[1, 3], [7, 9]]
D.[2, 8]
答案:B,
matrix[[0, 2], [1, 0]]
是配对索引。它会选择
matrix[0, 1]
(值为2
)和matrix[2, 0]
(值为7
)这两个元素。
(3)编程题
- 创建一个
4*4
的 NumPy 数组,元素为 1 到 16。 - 使用花式索引:
- 提取第 0 行、第 2 行和第 3 行。
- 提取位于
(0, 0)
,(1, 2)
,(2, 1)
,(3, 3)
位置的元素。 - 使用
np.ix_
提取由行索引[0, 2]
和列索引[1, 3]
定义的子矩阵。
- 打印每次操作后的结果。
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
27
28
arr = np.arange(1, 17).reshape(4, 4)
arr
"""
array([[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12],
[13, 14, 15, 16]])
"""
arr1 = arr[[0, 2, 3]]
arr1
"""
array([[ 1, 2, 3, 4],
[ 9, 10, 11, 12],
[13, 14, 15, 16]])
"""
arr2 = arr[[0, 1, 2, 3], [0, 2, 1, 3]]
arr2
"""
array([ 1, 7, 10, 16])
"""
row_index = [0, 2]
col_index = [1, 3]
arr3 = arr[np.ix_(row_index, col_index)]
arr3
"""
array([[ 2, 4],
[10, 12]])
"""
3.布尔值索引
布尔值索引是 NumPy 中一种非常强大且直观的数据筛选方式。它允许您使用一个与数组形状相同的布尔数组来选择元素。布尔数组中值为 True
的位置对应的原始数组元素会被选中,而值为 False
的位置对应的元素则会被忽略。
创建布尔数组: 最常见的创建布尔数组的方式是使用比较运算符(如 ==
, >
, <
, >=
, <=
, !=
)对 NumPy 数组进行操作。这些操作会逐元素地进行比较,并返回一个与原始数组形状相同的布尔数组。
筛选元素: 将布尔数组作为索引传递给 NumPy 数组,即可筛选出所有对应位置为 True
的元素。
arr[boolean_array]
:返回一个一维数组,其中包含所有满足条件的元素。即使原始数组是多维的,结果也会被展平为一维。
结合逻辑运算符: 您可以使用 &
(与), |
(或), ~
(非) 等位运算符来组合多个布尔条件。注意: 在 NumPy 中,对于布尔数组的逻辑运算,必须使用 &
, |
, ~
而不是 Python 的 and
, or
, not
关键字,因为后者会尝试对整个布尔数组进行真值判断,而不是逐元素操作。
修改元素: 布尔索引也可以用于修改数组中满足特定条件的元素
布尔索引的底层机制涉及对数据进行逐元素的条件检查和数据复制。
- 条件评估: 当您执行
arr > 90
这样的比较操作时,NumPy 会在内部创建一个临时的布尔数组。这个布尔数组的每个元素都对应原始数组中相应位置的元素,如果条件满足则为True
,否则为False
。- 数据选择与复制: 当您将这个布尔数组作为索引传递给原始数组时,NumPy 会遍历布尔数组。对于每一个
True
值,它会找到原始数组中对应位置的元素,并将其复制到一个新的、通常是一维的 NumPy 数组中。- 返回副本: 类似于花式索引,布尔索引操作也会返回一个新的数组副本。这是因为被选中的元素可能在原始数组中是非连续的,为了将它们组织成一个新的连续数组,需要进行数据复制。
- 性能: 布尔索引需要额外的内存来存储布尔数组,并且涉及数据复制,因此在性能上可能不如直接的切片操作(视图),但它提供了极大的灵活性来筛选数据。
【1】一维数组的布尔索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
names = np.array(['softpo', 'Brandon', 'Will', 'Michael', 'Will', 'Ella', 'Daniel', 'softpo', 'Will', 'Brandon'])
names
"""
array(['softpo', 'Brandon', 'Will', 'Michael', 'Will', 'Ella', 'Daniel',
'softpo', 'Will', 'Brandon'], dtype='<U7')
"""
arr1 = names == "Will"
arr1
"""
array([False, False, True, False, True, False, False, False, True,
False])
"""
name_Will = names[arr1]
name_Will
"""
array(['Will', 'Will', 'Will'], dtype='<U7')
"""
# 验证name_Will是一个副本
print(name_Will.base) # None
name_Will.flags.owndata # True
【2】二维数组的布尔索引
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
arr = np.random.randint(0, 100, (5, 4))
print(arr)
"""
[[ 0 58 35 49]
[82 96 23 32]
[84 9 1 77]
[77 87 57 73]
[32 88 62 43]]
"""
# 大于50
arr_bigger_50_cond = arr > 50
print(arr_bigger_50)
"""
[53 51 61 56 60 63 76 84 69 61 92 86]
"""
arr_bigger_50 = arr[arr_bigger_50_cond]
print(arr_bigger_50) # 展平成1维
# 多个布尔条件 比如20-80
cond2 = (arr > 20) & (arr < 80)
arr_new = arr[cond2]
display(arr_new)
"""
array([58, 35, 49, 23, 32, 77, 77, 57, 73, 32, 62, 43])
"""
# 布尔索引赋值
arr1 = np.random.randint(0, 10, size=(3, 3))
display(arr1)
"""
array([[0, 1, 4],
[3, 8, 3],
[1, 4, 5]])
"""
# 将所有的偶数 设置为-1
arr1[arr1 % 2 == 0] = -1
arr1
"""
array([[-1, 1, -1],
[ 3, -1, 3],
[ 1, -1, 5]])
"""
(1)选择题
-
给定以下 NumPy 代码:
1 2 3
import numpy as np data = np.array([5, 12, 8, 20, 3]) filtered_data = data[data > 10]
filtered_data
的值是什么?A.
[12, 20]
B.[5, 8, 3]
C.[True, False, False, True, False]
D.[12, 8, 20]
答案:A,
data > 10
会生成布尔数组[False, True, False, True, False]
。使用此布尔数组索引
data
会选择对应True
位置的元素,即12
和20
。 -
以下哪种逻辑运算符在 NumPy 布尔索引中是正确的?
A.
arr[cond1 and cond2]
B.arr[cond1 & cond2]
C.arr[cond1 or cond2]
D.arr[not cond]
答案:B,在 NumPy 中,对布尔数组进行逻辑运算必须使用逐位运算符
&
(与),|
(或),~
(非)。
(2)编程题
- 创建一个
4*4
的 NumPy 数组,包含 0 到 15 的随机整数。 - 使用布尔索引:
- 提取所有能被 3 整除的元素。
- 将所有小于 5 的元素设置为 0。
- 将所有大于 10 的元素设置为其自身的两倍。
- 打印每次操作后的数组。
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
27
28
29
30
31
32
33
34
arr = np.random.randint(0, 16, (4, 4))
print(arr)
"""
[[ 5 1 5 6]
[12 11 7 2]
[ 7 10 1 15]
[ 4 2 4 4]]
"""
con1 = arr % 3 == 0
arr1 = arr[con1]
print(arr1)
"""
[ 6 12 15]
"""
arr2 = arr.copy()
arr2[arr2 < 5] = 0
print(arr2)
"""
[[ 5 0 5 6]
[12 11 7 0]
[ 7 10 0 15]
[ 0 0 0 0]]
"""
arr3 = arr.copy()
arr3[arr3 > 10] *= 2
print(arr3)
"""
[[ 5 1 5 6]
[24 22 7 2]
[ 7 10 1 30]
[ 4 2 4 4]]
"""