一、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] 中,rows 和 cols 会逐个配对,返回一维数组。
- 若要选取子矩阵(行的所有列组合),必须使用
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中一个非常实用的函数,有两种使用模式:
- 单参数模式
np.where(condition):返回满足条件的元素的索引元组
- 三参数模式
np.where(condition, x, y):根据条件从 x 或 y 中选择元素
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]
十、核心要点总结
- 一维索引三件套:正向索引(从0开始)、负向索引(从-1开始)、步长切片(
start:stop:step)覆盖所有常见的一维数组访问需求。
- 多维索引语法:使用逗号分隔各个维度
arr[row, col, depth],每个维度独立使用索引或切片语法。
- 视图 vs 副本:基本切片返回视图(共享内存、修改会互相影响),花式索引和布尔索引返回副本(独立内存、修改隔离)。用
np.shares_memory() 检测。
- 布尔索引最强大:条件筛选、组合条件(
&/|/~、必须加括号)、赋值修改三位一体,是数据清洗和预处理的利器。
- 花式索引灵活:整数列表/数组作为索引,支持重复索引,适用于不规则数据选取。用
np.ix_() 处理多维子矩阵。
- np.where 双模式:单参数返回条件索引,三参数实现向量化三目运算,替代Python循环提升效率。
- 降维规则:整数索引使该维度降维,切片保留该维度。理解这一点才能准确预知操作结果的形状。
- 性能意识:优先使用向量化操作(索引/切片/where/条件运算)而非Python循环,大数据集上性能差异可达100倍以上。
十一、进一步思考
索引与切片是NumPy高效计算的基石,但真正的进阶之路远不止于此:
进阶方向
- 广播机制:理解形状不同的数组如何自动对齐并进行运算——这是向量化计算的另一大支柱。
- 高级索引组合:将切片、花式索引和布尔索引混合使用,配合
np.newaxis 扩展维度,实现复杂的数据变换。
- 结构化数组:NumPy的结构化数组允许按字段名访问数据,像操作数据库表格一样操作数组。
- 性能优化:使用
numexpr 或 Numba 进一步加速大规模数组运算,突破NumPy自身的性能边界。
- 与其他库结合:Pandas的
.loc/.iloc 索引大量借鉴了NumPy的设计思想,学好NumPy索引将为学习Pandas打下坚实基础。