NumPy 数组索引和切片
数组索引和切片是 NumPy 中最常用的操作之一。本章将详细介绍各种索引和切片方法。
基本索引
一维数组索引
一维数组的索引方式与 Python 列表类似:
import numpy as np
arr = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
# 正向索引(从0开始)
print(f"arr[0]: {arr[0]}") # 第一个元素: 0
print(f"arr[3]: {arr[3]}") # 第四个元素: 3
# 负向索引(从-1开始)
print(f"arr[-1]: {arr[-1]}") # 最后一个元素: 9
print(f"arr[-3]: {arr[-3]}") # 倒数第三个元素: 7
# 索引越界会报错
# print(arr[10]) # IndexError
多维数组索引
多维数组使用逗号分隔的索引元组:
import numpy as np
# 创建二维数组
arr = np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]])
print(f"数组:\n{arr}")
# 使用逗号分隔各维度索引
print(f"arr[0, 0]: {arr[0, 0]}") # 第一行第一列: 1
print(f"arr[1, 2]: {arr[1, 2]}") # 第二行第三列: 7
print(f"arr[-1, -1]: {arr[-1, -1]}") # 最后一行最后一列: 12
# 只指定部分索引
print(f"arr[0]: {arr[0]}") # 整行,返回一维数组
print(f"arr[:, 0]: {arr[:, 0]}") # 整列,返回一维数组
# 三维数组
arr_3d = np.array([[[1, 2], [3, 4]],
[[5, 6], [7, 8]]])
print(f"三维数组形状: {arr_3d.shape}") # (2, 2, 2)
print(f"arr_3d[0]:\n{arr_3d[0]}") # 第一"页"
print(f"arr_3d[0, 1]: {arr_3d[0, 1]}") # 第一页第二行
print(f"arr_3d[0, 1, 0]: {arr_3d[0, 1, 0]}") # 3
切片操作
一维数组切片
切片语法为 start:stop:step,与 Python 列表完全一致:
import numpy as np
arr = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
# 基本切片
print(f"arr[2:5]: {arr[2:5]}") # [2, 3, 4] - 索引2到4(不含5)
print(f"arr[:5]: {arr[:5]}") # [0, 1, 2, 3, 4] - 从开头到4
print(f"arr[5:]: {arr[5:]}") # [5, 6, 7, 8, 9] - 从5到结尾
print(f"arr[:]: {arr[:]}") # [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - 全部
# 负索引切片
print(f"arr[-3:]: {arr[-3:]}") # [7, 8, 9] - 最后三个
print(f"arr[:-3]: {arr[:-3]}") # [0, 1, 2, 3, 4, 5, 6] - 除了最后三个
# 步长切片
print(f"arr[::2]: {arr[::2]}") # [0, 2, 4, 6, 8] - 每隔一个
print(f"arr[1::2]: {arr[1::2]}") # [1, 3, 5, 7, 9] - 从1开始
print(f"arr[::-1]: {arr[::-1]}") # [9, 8, 7, 6, 5, 4, 3, 2, 1, 0] - 反转
print(f"arr[5:2:-1]: {arr[5:2:-1]}") # [5, 4, 3] - 反向切片
# 省略参数
print(f"arr[::]: {arr[::]}") # 全部(等同于 arr[:])
print(f"arr[::3]: {arr[::3]}") # [0, 3, 6, 9] - 步长3
多维数组切片
多维数组的切片是每个维度独立切片的组合:
import numpy as np
arr = np.array([[1, 2, 3, 4, 5],
[6, 7, 8, 9, 10],
[11, 12, 13, 14, 15],
[16, 17, 18, 19, 20]])
print(f"数组 (4x5):\n{arr}")
# 各维度独立切片
print(f"arr[1:3, 2:4]:\n{arr[1:3, 2:4]}")
# 行1-2,列2-3
# 省略某些维度
print(f"arr[:, 0]: {arr[:, 0]}") # 所有行,第0列
print(f"arr[0, :]: {arr[0, :]}") # 第0行,所有列
print(f"arr[:, :]:\n{arr[:, :]}") # 全部
# 步长切片
print(f"arr[::2, ::2]:\n{arr[::2, ::2]}")
# 行间隔2,列间隔2
# 负索引切片
print(f"arr[-2:, -3:]:\n{arr[-2:, -3:]}")
# 最后2行,后3列
切片是视图而非副本
这是 NumPy 与 Python 列表的重要区别:切片返回的是视图,而不是副本:
import numpy as np
arr = np.array([1, 2, 3, 4, 5])
sliced = arr[1:4]
print(f"原数组: {arr}")
print(f"切片: {sliced}")
# 修改切片会影响原数组
sliced[0] = 99
print(f"修改切片后,原数组: {arr}") # [1, 99, 3, 4, 5]
# 如果需要副本,使用 copy()
arr2 = np.array([1, 2, 3, 4, 5])
sliced_copy = arr2[1:4].copy()
sliced_copy[0] = 99
print(f"修改副本后,原数组: {arr2}") # [1, 2, 3, 4, 5] - 不变
高级索引
整数数组索引
使用整数数组作为索引,可以自由选择任意元素:
import numpy as np
arr = np.array([10, 20, 30, 40, 50, 60])
# 一维整数数组索引
indices = [0, 2, 4]
print(f"arr[[0, 2, 4]]: {arr[indices]}") # [10, 30, 50]
# 重复索引
indices2 = [0, 0, 1, 1, 2, 2]
print(f"重复索引: {arr[indices2]}") # [10, 10, 20, 20, 30, 30]
# 二维索引
arr_2d = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# 分别指定行索引和列索引
rows = [0, 1, 2]
cols = [2, 1, 0]
print(f"arr[rows, cols]: {arr_2d[rows, cols]}") # [3, 5, 7]
# 选择特定位置的元素
print(f"arr[[0, 0], [0, 2]]: {arr_2d[[0, 0], [0, 2]]}") # [1, 3]
布尔索引
使用布尔数组作为索引,选择符合条件的元素:
import numpy as np
arr = np.array([10, 20, 30, 40, 50, 60, 70, 80])
# 创建布尔条件
condition = arr > 40
print(f"条件: {condition}") # [False False False False False True True True]
# 使用布尔数组索引
print(f"arr[condition]: {arr[condition]}") # [50, 60, 70, 80]
# 常用写法:直接使用条件表达式
print(f"arr[arr > 40]: {arr[arr > 40]}") # [50, 60, 70, 80]
print(f"arr[(arr > 20) & (arr < 60)]: {arr[(arr > 20) & (arr < 60)]}") # [30, 40, 50]
# 二维数组布尔索引
arr_2d = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# 选择大于5的元素(返回一维数组)
print(f"arr_2d[arr_2d > 5]: {arr_2d[arr_2d > 5]}") # [6, 7, 8, 9]
# 按行条件选择
print(f"arr_2d[arr_2d[:, 0] > 3]:\n{arr_2d[arr_2d[:, 0] > 3]}")
# 选择第一列大于3的行
np.where 函数
np.where 可以根据条件返回值或索引:
import numpy as np
arr = np.array([10, 20, 30, 40, 50, 60])
# 基础用法:条件索引
indices = np.where(arr > 30)
print(f"大于30的索引: {indices}") # (array([3, 4, 5]),)
print(f"大于30的元素: {arr[indices]}") # [40, 50, 60]
# 三参数用法:类似三元表达式
result = np.where(arr > 30, arr * 2, arr)
print(f"三参数 where: {result}") # [10, 20, 30, 80, 100, 120]
# 二维数组
arr_2d = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
# 返回满足条件的索引
rows, cols = np.where(arr_2d > 5)
print(f"大于5的位置: 行{rows}, 列{cols}")
# 使用 where 创建掩码
mask = arr_2d > 5
print(f"掩码:\n{mask}")
花式索引
花式索引(Fancy Indexing)使用整数数组或布尔数组进行索引:
import numpy as np
arr = np.arange(20).reshape(4, 5)
print(f"原数组:\n{arr}")
# 整数数组索引 - 选择特定行
rows = [0, 2, 3]
print(f"选择行 [0, 2, 3]:\n{arr[rows]}")
# 整数数组索引 - 选择特定列
cols = [1, 3, 4]
print(f"选择列 [1, 3, 4]:\n{arr[:, cols]}")
# 组合使用
print(f"arr[rows][cols]:\n{arr[rows][:, cols]}")
# 更高效的方式:arr[rows, cols[:, np.newaxis]]
print(f"arr[rows, cols]:\n{arr[np.ix_(rows, cols)]}")
ix_ 函数
np.ix_ 用于创建网格索引,使得花式索引更清晰:
import numpy as np
arr = np.arange(12).reshape(3, 4)
print(f"原数组:\n{arr}")
# 选择特定行和列的交叉点
rows = [0, 2]
cols = [1, 3]
print(f"交叉点:\n{arr[np.ix_(rows, cols)]}")
# ix_ 确保生成正确的广播索引
# 等价于:
# arr[[0, 2]][:, [1, 3]]
高级索引函数
NumPy 提供了一组专门的索引函数,用于更灵活地提取和修改数组元素。这些函数在处理复杂数据操作时非常有用。
np.take - 沿轴提取元素
np.take 沿指定轴从数组中提取元素,比花式索引更灵活:
import numpy as np
arr = np.array([4, 3, 5, 7, 6, 8])
# 基本用法:提取指定索引位置的元素
indices = [0, 1, 4]
result = np.take(arr, indices)
print(f"提取索引 [0, 1, 4] 的元素: {result}") # [4, 3, 6]
# 等价的花式索引写法
print(f"等价的花式索引: {arr[indices]}")
# 二维数组沿轴提取
arr_2d = np.arange(12).reshape(3, 4)
print(f"\n二维数组:\n{arr_2d}")
# 沿 axis=0 提取(提取行)
rows_to_take = [0, 2]
result_axis0 = np.take(arr_2d, rows_to_take, axis=0)
print(f"\n提取第0、2行:\n{result_axis0}")
# 沿 axis=1 提取(提取列)
cols_to_take = [1, 3]
result_axis1 = np.take(arr_2d, cols_to_take, axis=1)
print(f"提取第1、3列:\n{result_axis1}")
# 不指定 axis 时,先展平再提取
result_flat = np.take(arr_2d, [0, 5, 11])
print(f"\n展平后提取 [0, 5, 11]: {result_flat}")
mode 参数:处理越界索引
import numpy as np
arr = np.array([10, 20, 30, 40, 50])
# 'raise' 模式(默认):越界报错
try:
result = np.take(arr, [0, 10])
except IndexError as e:
print(f"'raise' 模式越界报错: {e}")
# 'clip' 模式:越界索引截断到有效范围
result_clip = np.take(arr, [0, 10], mode='clip')
print(f"\n'clip' 模式: {result_clip}") # 10 被截断为 4,取 arr[4]=50
# 'wrap' 模式:越界索引循环映射
result_wrap = np.take(arr, [0, 6, 12], mode='wrap')
print(f"'wrap' 模式: {result_wrap}") # 6%5=1, 12%5=2
np.take_along_axis - 匹配索引提取
np.take_along_axis 根据索引数组和数据数组的形状匹配来提取元素,常与 np.argsort 配合使用:
import numpy as np
# 场景:对每行按值排序,获取排序后的值
arr = np.array([[3, 1, 4],
[6, 5, 2]])
# 获取每行的排序索引
sort_indices = np.argsort(arr, axis=1)
print(f"排序索引:\n{sort_indices}")
# 使用 take_along_axis 获取排序后的值
sorted_arr = np.take_along_axis(arr, sort_indices, axis=1)
print(f"\n每行排序后:\n{sorted_arr}")
# 类似地,按列排序
sort_indices_col = np.argsort(arr, axis=0)
sorted_arr_col = np.take_along_axis(arr, sort_indices_col, axis=0)
print(f"\n每列排序后:\n{sorted_arr_col}")
# 获取每行的前 k 小元素
k = 2
topk_indices = np.argpartition(arr, k, axis=1)[:, :k]
topk_values = np.take_along_axis(arr, topk_indices, axis=1)
print(f"\n每行最小的 {k} 个元素:\n{topk_values}")
np.choose - 按索引选择构造数组
np.choose 根据索引数组从多个选项中构造新数组:
import numpy as np
# 基本用法:根据索引选择元素
choices = [[0, 1, 2, 3],
[10, 11, 12, 13],
[20, 21, 22, 23]]
indices = [2, 0, 1, 2] # 选择第2、0、1、2个数组的对应位置元素
result = np.choose(indices, choices)
print(f"选择结果: {result}") # [20, 1, 12, 23]
# 解释:result[0]=choices[2][0]=20, result[1]=choices[0][1]=1, ...
# 二维索引数组
indices_2d = [[0, 1],
[2, 0]]
result_2d = np.choose(indices_2d, choices)
print(f"\n二维选择:\n{result_2d}")
# mode 参数处理越界索引
indices_oob = [0, 1, 2, 3, 4] # 4 越界(只有 0-2 三个选项)
result_clip = np.choose(indices_oob, choices, mode='clip')
print(f"\n'clip' 模式处理越界: {result_clip}") # 4 被截断为 2
result_wrap = np.choose(indices_oob, choices, mode='wrap')
print(f"'wrap' 模式处理越界: {result_wrap}") # 4 % 3 = 1
实战示例:颜色映射
import numpy as np
# 假设有分类标签(0, 1, 2, 3)
labels = np.array([[0, 1, 2],
[3, 1, 0]])
# 每个类别对应的颜色
colors = [
[255, 0, 0], # 类别0: 红色
[0, 255, 0], # 类别1: 绿色
[0, 0, 255], # 类别2: 蓝色
[255, 255, 0] # 类别3: 黄色
]
# 使用 choose 将标签映射为颜色
image = np.choose(labels, colors)
print(f"标签映射为颜色:\n{image}")
np.select - 多条件选择
np.select 根据多个条件从多个选项中选择,比嵌套 np.where 更清晰:
import numpy as np
# 成绩数据
scores = np.array([55, 72, 85, 91, 68, 45, 88])
# 定义条件和对应值
conditions = [
scores < 60, # 不及格
scores < 70, # 及格
scores < 80, # 中等
scores < 90, # 良好
scores >= 90 # 优秀
]
choices = [
'F', # 不及格
'D', # 及格
'C', # 中等
'B', # 良好
'A' # 优秀
]
# 根据条件选择等级
grades = np.select(conditions, choices, default='N/A')
print(f"成绩: {scores}")
print(f"等级: {grades}")
# 使用 default 参数设置默认值
scores2 = np.array([55, 72, 85, 91])
conditions2 = [
scores2 < 60,
scores2 >= 80
]
choices2 = ['不及格', '优秀']
result = np.select(conditions2, choices2, default='中等')
print(f"\n带默认值: {result}") # [不及格, 中等, 优秀, 优秀]
实战示例:数据分箱
import numpy as np
# 年龄数据
ages = np.array([5, 12, 18, 25, 35, 50, 65, 80])
# 年龄分组
conditions = [
ages < 13, # 儿童
(ages >= 13) & (ages < 20), # 青少年
(ages >= 20) & (ages < 40), # 青年
(ages >= 40) & (ages < 60), # 中年
ages >= 60 # 老年
]
groups = ['儿童', '青少年', '青年', '中年', '老年']
age_groups = np.select(conditions, groups)
print("年龄分组:")
for age, group in zip(ages, age_groups):
print(f" {age}岁: {group}")
np.extract - 提取满足条件的元素
np.extract 从数组中提取满足条件的元素:
import numpy as np
arr = np.arange(12).reshape(3, 4)
print(f"原数组:\n{arr}")
# 提取大于 5 的元素
condition = arr > 5
extracted = np.extract(condition, arr)
print(f"\n大于 5 的元素: {extracted}")
# 与布尔索引等价
print(f"布尔索引结果: {arr[condition]}")
# 提取偶数
even_mask = arr % 2 == 0
even_numbers = np.extract(even_mask, arr)
print(f"\n偶数元素: {even_numbers}")
np.put - 替换指定位置的元素
np.put 原地替换数组中指定位置的元素(在展平后的数组上操作):
import numpy as np
arr = np.arange(5)
print(f"原数组: {arr}")
# 替换索引 0 和 2 位置的元素
np.put(arr, [0, 2], [-44, -55])
print(f"替换后: {arr}") # [-44, 1, -55, 3, 4]
# 二维数组(展平后操作)
arr_2d = np.arange(12).reshape(3, 4)
print(f"\n二维数组:\n{arr_2d}")
np.put(arr_2d, [0, 5, 11], [100, 200, 300])
print(f"替换索引 0, 5, 11 后:\n{arr_2d}")
# mode 参数处理越界索引
arr3 = np.arange(5)
np.put(arr3, 10, -1, mode='clip') # 索引 10 被截断为 4
print(f"\n'clip' 模式: {arr3}")
arr4 = np.arange(5)
np.put(arr4, 7, -1, mode='wrap') # 索引 7 % 5 = 2
print(f"'wrap' 模式: {arr4}")
np.place - 根据条件替换元素
np.place 根据布尔掩码替换数组中的元素:
import numpy as np
arr = np.arange(12).reshape(3, 4)
print(f"原数组:\n{arr}")
# 将大于 5 的元素替换为 -1
np.place(arr, arr > 5, [-1])
print(f"\n大于 5 的替换为 -1:\n{arr}")
# 使用多个替换值(循环使用)
arr2 = np.arange(10)
np.place(arr2, arr2 % 2 == 0, [100, 200])
print(f"\n偶数位置替换为 100, 200 循环: {arr2}")
# 复杂条件
arr3 = np.random.randint(0, 100, 15)
print(f"\n随机数组: {arr3}")
np.place(arr3, (arr3 > 50) & (arr3 < 80), [-1, -2])
print(f"50-80之间的替换: {arr3}")
np.putmask - 根据掩码替换元素
np.putmask 与 np.place 类似,但替换值需要与数组形状匹配:
import numpy as np
arr = np.arange(6)
print(f"原数组: {arr}")
# 创建掩码
mask = arr % 2 == 0 # 偶数位置
# 替换值数组
values = np.array([100, 200, 300])
np.putmask(arr, mask, values)
print(f"偶数位置替换: {arr}") # [100, 1, 200, 3, 300, 5]
# putmask vs place 的区别
arr1 = np.arange(6)
arr2 = np.arange(6)
mask = arr1 > 2
values = [10, 20, 30]
# place: 只使用前 N 个值(N 是掩码中 True 的数量)
np.place(arr1, mask, values)
print(f"\nplace 结果: {arr1}") # [0, 1, 2, 10, 20, 30]
# putmask: 值数组需要能广播到原数组形状
np.putmask(arr2, mask, np.array([10, 20, 30]))
print(f"putmask 结果: {arr2}")
np.put_along_axis - 沿轴替换元素
np.put_along_axis 是 np.take_along_axis 的逆操作:
import numpy as np
# 创建数组
arr = np.zeros((3, 4))
print(f"原数组:\n{arr}")
# 要放置的值和索引
values = np.array([[1, 2, 3, 4],
[5, 6, 7, 8],
[9, 10, 11, 12]])
# 每行放置值的列索引
indices = np.array([[0, 1, 2, 3],
[3, 2, 1, 0],
[1, 3, 0, 2]])
# 沿 axis=1 放置值
np.put_along_axis(arr, indices, values, axis=1)
print(f"\n沿轴放置后:\n{arr}")
# 实际应用:将每行的最大值设为 0
arr2 = np.random.rand(3, 4)
print(f"\n随机数组:\n{arr2}")
# 找每行最大值的列索引
max_indices = np.argmax(arr2, axis=1, keepdims=True)
print(f"每行最大值位置: {max_indices.T}")
# 将最大值设为 0
np.put_along_axis(arr2, max_indices, 0, axis=1)
print(f"最大值设为 0 后:\n{arr2}")
高级索引函数对照表
| 函数 | 操作类型 | 说明 |
|---|---|---|
np.take | 提取 | 沿轴提取元素,支持越界处理 |
np.take_along_axis | 提取 | 按匹配索引提取,常配合 argsort |
np.choose | 构造 | 按索引从选项中构造数组 |
np.select | 构造 | 多条件选择,替代嵌套 where |
np.extract | 提取 | 提取满足条件的元素 |
np.put | 修改 | 原地替换指定位置元素 |
np.place | 修改 | 按条件替换元素 |
np.putmask | 修改 | 按掩码替换元素 |
np.put_along_axis | 修改 | 沿轴按索引放置元素 |
索引和切片的选择
| 场景 | 推荐方法 | 说明 |
|---|---|---|
| 选择连续区域 | 切片 arr[1:3, 2:4] | 返回视图,性能好 |
| 选择特定元素 | 整数索引 arr[[0, 2], [1, 3]] | 返回副本 |
| 按条件选择 | 布尔索引 arr[arr > 0] | 灵活方便 |
| 复杂选择 | np.where + 整数索引 | 组合使用 |
import numpy as np
arr = np.arange(12).reshape(3, 4)
print(f"数组:\n{arr}")
# 选择连续区域(推荐切片)
region = arr[1:, 2:]
print(f"切片区域:\n{region}")
# 选择分散位置(使用花式索引)
disperse = arr[[0, 2], [0, 3]]
print(f"分散位置: {disperse}")
# 复杂条件(使用布尔索引)
complex_condition = arr[(arr > 3) & (arr < 10)]
print(f"复杂条件: {complex_condition}")
小结
本章介绍了 NumPy 的各种索引和切片方法:
- 基本索引:使用整数索引访问单个元素
- 切片操作:使用
start:stop:step语法,切片返回视图 - 高级索引:
- 整数数组索引:自由选择元素,返回副本
- 布尔索引:按条件筛选
- np.where:根据条件获取索引或值
- 花式索引:使用整数数组进行复杂选择
理解这些索引方法的区别(视图 vs 副本)对于正确操作 NumPy 数组非常重要。