NumPy索引与切片

灵活访问数组元素

一、NumPy索引概述

NumPy是Python数据科学生态中最核心的库之一,其提供的ndarray(N维数组)对象是几乎所有数值计算的基础。而索引与切片是操作NumPy数组最基础也最重要的技能——它决定了我们如何高效、精准地访问和修改数组中的元素。

与Python原生的列表相比,NumPy的索引系统更加强大和灵活:它不仅支持常规的整数索引和切片,还支持花式索引(Fancy Indexing)和布尔索引(Boolean Indexing),能够用非常简洁的语法实现复杂的数据筛选和变换。

核心观点

  • NumPy索引的核心语法基于 方括号 [],但扩展出了多维索引、切片、花式索引和布尔索引四种模式
  • 视图(View) vs 复制(Copy) 是理解切片行为的关键——基本切片返回视图,花式索引和布尔索引返回副本
  • 熟练掌握索引与切片可以将数据处理代码从循环写法(慢)转换为向量化写法(快),性能提升几个数量级

二、一维数组索引

一维数组的索引与Python列表的索引完全兼容,支持正向索引负向索引步长切片三种方式。

2.1 正向索引

正向索引从 0 开始,依次递增。索引 i 访问数组中第 i+1 个元素。

import numpy as np arr = np.array([10, 20, 30, 40, 50, 60, 70, 80, 90, 100]) # 正向索引 print(arr[0]) # 第一个元素 print(arr[3]) # 第四个元素 print(arr[9]) # 第十个元素
>>> 10 >>> 40 >>> 100

2.2 负向索引

负向索引从 -1 开始,-1 表示最后一个元素,-2 表示倒数第二个,以此类推。

# 负向索引 print(arr[-1]) # 最后一个元素 → 100 print(arr[-3]) # 倒数第三个元素 → 80 print(arr[-10]) # 倒数第十个 → 10(等同于 arr[0])
>>> 100 >>> 80 >>> 10

2.3 步长切片

语法格式:arr[start:stop:step],三个参数均可省略。

# 基本切片 print(arr[2:7]) # 索引2到6(不包含7)→ [30 40 50 60 70] # 省略起始 print(arr[:5]) # 前五个元素 → [10 20 30 40 50] # 省略结束 print(arr[6:]) # 索引6到最后 → [70 80 90 100] # 步长为2(取出偶数索引元素) print(arr[::2]) # [10 30 50 70 90] # 反向步长(反转数组) print(arr[::-1]) # [100 90 80 70 60 50 40 30 20 10] # 从索引1到8,步长3 print(arr[1:9:3]) # [20 50 80]
>>> [30 40 50 60 70] >>> [10 20 30 40 50] >>> [70 80 90 100] >>> [10 30 50 70 90] >>> [100 90 80 70 60 50 40 30 20 10] >>> [20 50 80]

切片规则速记

左闭右开arr[start:stop] 包含 start 位置,不包含 stop 位置。

负数索引结合切片arr[-5:-2] 表示倒数第5个到倒数第3个(不含),相当于 arr[5:8]

全复制语法arr[:] 不指定起止,表示整个数组的视图。

三、多维数组索引

二维数组(矩阵)的索引使用 arr[row, col] 的逗号分隔语法,第一个维度是行,第二个维度是列。更高维数组依此类推。

3.1 创建多维数组

# 创建 3x4 的二维数组 matrix = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]) print("数组形状:", matrix.shape) print("数组维度:", matrix.ndim)
>>> 数组形状: (3, 4) >>> 数组维度: 2

3.2 元素访问 [row, col]

# 访问单个元素 —— 第2行第3列的值 print(matrix[1, 2]) # 7 # 访问第1行整行 print(matrix[0, :]) # [1 2 3 4] # 访问第3列整列 print(matrix[:, 2]) # [3 7 11] # 访问子矩阵:第1-2行,第2-3列 print(matrix[0:2, 1:3]) # [[2 3] [6 7]]
>>> 7 >>> [1 2 3 4] >>> [3 7 11] >>> [[2 3] [6 7]]

3.3 三维数组索引

# 创建 2x3x4 的三维数组 arr_3d = np.arange(24).reshape(2, 3, 4) print("三维数组:") print(arr_3d) # 访问第0个"深度层" print("\n第0个深度层:") print(arr_3d[0]) # 访问第0个深度层、第1行、第2列 print("\narr_3d[0, 1, 2] =", arr_3d[0, 1, 2]) # 6
>>> 三维数组: [[[ 0 1 2 3] [ 4 5 6 7] [ 8 9 10 11]] [[12 13 14 15] [16 17 18 19] [20 21 22 23]]] >>> 第0个深度层: [[ 0 1 2 3] [ 4 5 6 7] [ 8 9 10 11]] >>> arr_3d[0, 1, 2] = 6

重要区别

matrix[1]matrix[1, :] 在二维数组中结果相同,都是取第1行;但在高维数组中 arr[1] 只取第一个维度的切片,而 arr[1, :] 明确指定了所有维度的范围,语义更清晰。

四、切片操作深入

4.1 子数组视图

NumPy切片返回的是原始数组的视图(View),而不是副本。这意味着对切片的修改会直接影响原始数组。这一设计是为了内存效率——避免大数据集的不必要复制。

arr = np.array([10, 20, 30, 40, 50, 60]) # 创建切片(视图) view = arr[2:5] print("视图:", view) # [30 40 50] # 修改视图中的元素 view[0] = 999 print("修改后视图:", view) # [999 40 50] print("原始数组:", arr) # [10 20 999 40 50 60] ← 原始数组也被修改了!
>>> 视图: [30 40 50] >>> 修改后视图: [999 40 50] >>> 原始数组: [10 20 999 40 50 60]

4.2 步进切片

步进切片可以用非常简洁的语法实现复杂的数据选取模式。

# 创建1到20的数组 arr = np.arange(1, 21) print("原始数组:", arr) # 每隔一个取一个(取奇数索引位置) print("arr[1::2] =", arr[1::2]) # [2 4 6 8 10 12 14 16 18 20] # 每隔三个取一个 print("arr[::3] =", arr[::3]) # [1 4 7 10 13 16 19] # 反向步长:从后往前每隔2个取一个 print("arr[::-2] =", arr[::-2]) # [20 18 16 14 12 10 8 6 4 2]
>>> 原始数组: [ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20] >>> arr[1::2] = [ 2 4 6 8 10 12 14 16 18 20] >>> arr[::3] = [ 1 4 7 10 13 16 19] >>> arr[::-2] = [20 18 16 14 12 10 8 6 4 2]

4.3 多维切片

matrix = np.arange(1, 13).reshape(3, 4) print("矩阵:") print(matrix) # 取所有行、第1-3列(排除第0列和第3列) print("\n所有行,第1-3列:") print(matrix[:, 1:3]) # 取第1-2行、所有列 print("\n第1-2行,所有列:") print(matrix[1:3, :]) # 取第0行、第1-3列 print("\n第0行,第1-3列:") print(matrix[0, 1:3]) # 取所有行的第0列和第2列(步长为2) print("\n所有行,第0和2列:") print(matrix[:, ::2])
>>> 矩阵: [[ 1 2 3 4] [ 5 6 7 8] [ 9 10 11 12]] >>> 所有行,第1-3列: [[ 2 3] [ 6 7] [10 11]] >>> 第1-2行,所有列: [[ 5 6 7 8] [ 9 10 11 12]] >>> 第0行,第1-3列: [2 3] >>> 所有行,第0和2列: [[ 1 3] [ 5 7] [ 9 11]]

切片维度规则

当切片只作用于部分维度时,被"完整切片"的维度(即 : 覆盖整个范围的维度)不会降维。而使用单个整数索引的维度会降维。例如 matrix[0, :] 返回一维数组(第0行被提取了出来),而 matrix[0:1, :] 返回二维数组(仍然是行的切片)。

五、花式索引

花式索引(Fancy Indexing)允许我们使用整数数组或列表作为索引,一次性选取多个不连续的元素。这是普通切片无法做到的。

5.1 一维花式索引

arr = np.array([10, 20, 30, 40, 50, 60, 70, 80]) # 使用列表选取多个元素 indices = [0, 3, 5] print(arr[indices]) # [10 40 60] # 使用数组选取(结果形状与索引数组相同) print(arr[np.array([1, 4, 6])]) # [20 50 70] # 花式索引支持重复索引 print(arr[[1, 1, 2, 2, 3, 3]]) # [20 20 30 30 40 40] # 选取除特定元素外的所有元素 print(arr[np.delete(np.arange(8), [2, 5])]) # 删除索引2和5
>>> [10 40 60] >>> [20 50 70] >>> [20 20 30 30 40 40] >>> [10 20 40 50 70 80]

5.2 二维花式索引

matrix = np.arange(1, 13).reshape(4, 3) print("矩阵:") print(matrix) # 选取特定行的子集 print("\n选取第0行和第2行:") print(matrix[[0, 2]]) # 行列同时使用花式索引 # 选取 (0,2), (1,1), (3,0) 三个位置的元素 rows = [0, 1, 3] cols = [2, 1, 0] print("\n选取 (0,2),(1,1),(3,0):") print(matrix[rows, cols]) # [3 5 10] # 如果想选取的是"子矩阵"(行的所有列组合),需要使用 np.ix_ print("\n选取行子矩阵:") print(matrix[np.ix_([0, 2], [0, 2])]) # [[1 3] [7 9]]
>>> 矩阵: [[ 1 2 3] [ 4 5 6] [ 7 8 9] [10 11 12]] >>> 选取第0行和第2行: [[1 2 3] [7 8 9]] >>> 选取 (0,2),(1,1),(3,0): [3 5 10] >>> 选取行子矩阵: [[1 3] [7 9]]

花式索引关键注意事项

  • 花式索引返回副本,不是视图。修改结果不会影响原始数组。
  • arr[[0, 2]] 返回 新数组(副本),修改它不会影响 arr
  • matrix[rows, cols] 中,rowscols逐个配对,返回一维数组。
  • 若要选取子矩阵(行的所有列组合),必须使用 np.ix_()np.outer() 辅助。

六、布尔索引

布尔索引是NumPy最强大的功能之一。它允许我们使用布尔值数组来筛选数据——只有对应位置为 True 的元素会被选中。这使得条件筛选变得极为简洁和高效。

6.1 基本条件筛选

arr = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) # 创建布尔掩码 mask = arr > 5 print("布尔掩码:", mask) # [False False False False False True True True True True] # 应用布尔掩码 print("arr > 5 的元素:", arr[mask]) # [ 6 7 8 9 10] # 直接在方括号内写条件 print("arr % 2 == 0:", arr[arr % 2 == 0]) # [ 2 4 6 8 10]
>>> 布尔掩码: [False False False False False True True True True True] >>> arr > 5 的元素: [ 6 7 8 9 10] >>> arr % 2 == 0: [ 2 4 6 8 10]

6.2 组合条件

组合条件需要使用 &(与)、|(或)、~(非)操作符,并且必须用括号包围每个条件

# 组合条件:大于3且小于8 condition = (arr > 3) & (arr < 8) print("大于3且小于8:", arr[condition]) # [4 5 6 7] # 组合条件:小于3或大于8 condition = (arr < 3) | (arr > 8) print("小于3或大于8:", arr[condition]) # [1 2 9 10] # 使用 ~ 取反(不在条件内的元素) condition = (arr > 5) print("不大于5:", arr[~condition]) # [1 2 3 4 5]
>>> 大于3且小于8: [4 5 6 7] >>> 小于3或大于8: [1 2 9 10] >>> 不大于5: [1 2 3 4 5]

6.3 二维数组的布尔索引

matrix = np.arange(1, 13).reshape(4, 3) print("矩阵:") print(matrix) # 筛选出所有大于6的元素 print("\n大于6的元素:", matrix[matrix > 6]) # [ 7 8 9 10 11 12] # 整行筛选:选出所有第二列大于5的行 mask = matrix[:, 1] > 5 print("\n第二列大于5的行:") print(matrix[mask]) # [[ 7 8 9] # [10 11 12]] # 将满足条件的元素替换为新值 matrix[matrix % 2 == 0] = -1 print("\n将所有偶数替换为-1:") print(matrix)
>>> 矩阵: [[ 1 2 3] [ 4 5 6] [ 7 8 9] [10 11 12]] >>> 大于6的元素: [ 7 8 9 10 11 12] >>> 第二列大于5的行: [[ 7 8 9] [10 11 12]] >>> 将所有偶数替换为-1: [[ 1 -1 3] [-1 5 -1] [ 7 -1 9] [-1 11 -1]]

布尔索引注意事项

  • 布尔索引返回副本,不是视图。但可以对原始数组使用布尔索引进行赋值操作(如上例),此时会直接修改原始数组。
  • 组合条件时必须使用括号(arr > 3) & (arr < 8),不能写成 arr > 3 & arr < 8
  • 不能使用Python的 and/or/not,必须使用 &/|/~

6.4 布尔索引实战案例

# 生成10000个正态分布随机数 data = np.random.randn(10000) # 筛选出超出2个标准差的数据(异常值) outliers = data[np.abs(data) > 2] print(f"异常值数量: {len(outliers)}") print(f"异常值占比: {len(outliers)/len(data)*100:.2f}%") # 将异常值替换为边界值(截断处理) data_clipped = data.copy() data_clipped[data_clipped > 2] = 2 data_clipped[data_clipped < -2] = -2 print(f"截断后范围: [{data_clipped.min():.2f}, {data_clipped.max():.2f}]")
>>> 异常值数量: 458 >>> 异常值占比: 4.58% >>> 截断后范围: [-2.00, 2.00]

七、切片效率与视图 vs 复制

理解视图(View)和复制(Copy)的区别对于编写高效的NumPy代码至关重要。错误的认知可能导致意外的内存膨胀或数据污染。

7.1 视图与复制的判定规则

操作类型返回结果说明
基本切片 arr[1:5]视图步长 > 0 的基本切片返回视图
步进切片 arr[::2]视图带步长的切片在NumPy中可能返回视图
整数索引 arr[0]降维视图返回标量或降维后的视图
花式索引 arr[[0,2]]副本使用列表/数组作为索引返回副本
布尔索引 arr[mask]副本布尔掩码返回副本
arr.copy()副本显式复制
# 检查是视图还是副本 arr = np.array([1, 2, 3, 4, 5]) slice_view = arr[1:4] # 基本切片 → 视图 fancy_copy = arr[[1, 2, 3]] # 花式索引 → 副本 print("slice_view 是视图吗?", np.shares_memory(arr, slice_view)) # True print("fancy_copy 是视图吗?", np.shares_memory(arr, fancy_copy)) # False
>>> slice_view 是视图吗? True >>> fancy_copy 是视图吗? False

7.2 内存效率对比

# 创建大数组并测试内存 big_arr = np.random.randn(10_000_000) # 视图:几乎不占用额外内存 view = big_arr[::2] print(f"视图大小: {view.nbytes / 1e6:.1f} MB") print(f"视图共享内存: {np.shares_memory(big_arr, view)}") # 副本:占用独立内存 copy = big_arr[::2].copy() print(f"副本大小: {copy.nbytes / 1e6:.1f} MB") print(f"副本共享内存: {np.shares_memory(big_arr, copy)}")
>>> 视图大小: 40.0 MB >>> 视图共享内存: True >>> 副本大小: 40.0 MB >>> 副本共享内存: False

何时需要显式复制

视图的"写回"特性在以下场景可能引发隐晦的bug:

  • 在函数内修改了视图,期望不影响外部数组
  • 从大数组中切出子数组后继续使用,但原数组被释放——视图仍持有对原数据的引用,导致内存无法回收
  • 对切片结果做后续花式索引或布尔索引前,不需要保持原数组同步

在这些情况下,应当使用 .copy() 显式创建副本。

八、np.where 条件选择

np.where 是NumPy中一个非常实用的函数,有两种使用模式:

8.1 查找满足条件的索引

arr = np.array([3, 7, 1, 9, 4, 6, 8, 2, 5, 0]) # 找出所有大于5的元素的索引 indices = np.where(arr > 5) print("大于5的索引:", indices) print("对应的元素:", arr[indices]) # 索引: (array([1, 3, 5, 6, 8]),) # 对应的元素: [7 9 6 8 5]
>>> 大于5的索引: (array([1, 3, 5, 6]),) >>> 对应的元素: [7 9 6 8]

8.2 条件三目运算

arr = np.array([1, -2, 3, -4, 5, -6, 7, -8, 9, -10]) # 将负数替换为0,正数保持不变 result = np.where(arr > 0, arr, 0) print("负数变0:", result) # [1 0 3 0 5 0 7 0 9 0] # 更复杂的条件选择:正数×2,负数×(-1) result2 = np.where(arr > 0, arr * 2, arr * (-1)) print("条件变换:", result2) # [2 2 6 4 10 6 14 8 18 10]
>>> 负数变0: [1 0 3 0 5 0 7 0 9 0] >>> 条件变换: [ 2 2 6 4 10 6 14 8 18 10]

8.3 多维数组的 np.where

matrix = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) # 找出所有大于5的元素位置 rows, cols = np.where(matrix > 5) print("大于5的行索引:", rows) print("大于5的列索引:", cols) print("对应的值:", matrix[rows, cols]) # 行: [1 2 2 2] # 列: [2 0 1 2] # 值: [6 7 8 9] # 条件选择:对角线元素替换为0,其他保持不变 mask = np.eye(3, dtype=bool) result = np.where(mask, 0, matrix) print("\n对角线置零:") print(result)
>>> 大于5的行索引: [1 2 2 2] >>> 大于5的列索引: [2 0 1 2] >>> 对应的值: [6 7 8 9] >>> 对角线置零: [[0 2 3] [4 0 6] [7 8 0]]

np.where 的高效使用

  • 替代循环np.where 的执行速度远快于Python的 for 循环,因为底层是C语言实现。
  • 嵌套 where:可以实现多层条件判断,但要注意可读性。
  • 与布尔索引的区别np.where 返回索引(可以后续使用),布尔索引直接获取值。

8.4 np.where 实战应用

# 学生成绩数据分析 scores = np.array([58, 72, 85, 43, 91, 67, 39, 78, 94, 62]) # 等级划分 grades = np.where(scores >= 90, '优秀', np.where(scores >= 75, '良好', np.where(scores >= 60, '及格', '不及格'))) print("成绩:", scores) print("等级:", grades) # 找出需要补考的学生(不及格) fail_idx = np.where(scores < 60)[0] print(f"\n需要补考的学生索引: {fail_idx}") print(f"补考成绩: {scores[fail_idx]}")
>>> 成绩: [58 72 85 43 91 67 39 78 94 62] >>> 等级: ['不及格' '及格' '良好' '不及格' '优秀' '及格' '不及格' '良好' '优秀' '及格'] >>> 需要补考的学生索引: [0 3 6] >>> 补考成绩: [58 43 39]

九、综合实战案例

案例:金融数据分析——筛选并处理股票数据

假设我们有一个包含多只股票每日涨跌幅数据的二维数组,我们需要进行一系列索引与切片操作。

# 模拟5只股票20个交易日的涨跌幅数据(百分比) np.random.seed(42) stock_data = np.random.randn(20, 5) * 2 # 乘以2模拟更大波动 # 提取前10个交易日的所有股票数据 first_10_days = stock_data[:10, :] print("前10天数据形状:", first_10_days.shape) # 找出所有涨幅超过3%的交易日和股票 big_gain_mask = stock_data > 3 big_gain_days, big_gain_stocks = np.where(big_gain_mask) print(f"共 {len(big_gain_days)} 次涨幅超过3%") # 计算每只股票的日均涨跌幅 avg_daily = np.mean(stock_data, axis=0) print("每只股票日均涨跌幅:", np.round(avg_daily, 2)) # 选出日均涨跌幅为正的股票 positive_stocks = avg_daily > 0 print("日均涨跌幅为正的股票索引:", np.where(positive_stocks)[0]) # 提取这些股票的全部数据 positive_data = stock_data[:, positive_stocks] print("正向股票数据形状:", positive_data.shape) # 找出所有交易中同时满足:跌幅超过2% 且 是第3只股票的交易 stock_3_mask = stock_data[:, 2] < -2 bad_days = np.where(stock_3_mask)[0] print(f"第3只股票跌幅超过2%的天数: {len(bad_days)}") # 将极端值(|涨跌幅| > 4%)替换为4或-4 clipped_data = stock_data.copy() clipped_data[clipped_data > 4] = 4 clipped_data[clipped_data < -4] = -4 print("极端值截断后范围:", f"[{clipped_data.min():.2f}, {clipped_data.max():.2f}]")
>>> 前10天数据形状: (10, 5) >>> 共 3 次涨幅超过3% >>> 每只股票日均涨跌幅: [ 0.14 0.22 -0.04 0.37 -0.12] >>> 日均涨跌幅为正的股票索引: [0 1 3] >>> 正向股票数据形状: (20, 3) >>> 第3只股票跌幅超过2%的天数: 4 >>> 极端值截断后范围: [-4.00, 4.00]

十、核心要点总结

十一、进一步思考

索引与切片是NumPy高效计算的基石,但真正的进阶之路远不止于此:

进阶方向

  • 广播机制:理解形状不同的数组如何自动对齐并进行运算——这是向量化计算的另一大支柱。
  • 高级索引组合:将切片、花式索引和布尔索引混合使用,配合 np.newaxis 扩展维度,实现复杂的数据变换。
  • 结构化数组:NumPy的结构化数组允许按字段名访问数据,像操作数据库表格一样操作数组。
  • 性能优化:使用 numexprNumba 进一步加速大规模数组运算,突破NumPy自身的性能边界。
  • 与其他库结合:Pandas的 .loc/.iloc 索引大量借鉴了NumPy的设计思想,学好NumPy索引将为学习Pandas打下坚实基础。