2.2 索引、切片与布尔掩码
上一节为数组声明了形状和轴的含义。现在,选择数据就变成了坐标问题:知道值的位置时使用整数索引,位置形成规则区间时使用切片,是否入选取决于值本身时使用布尔掩码。
继续使用一张五天 × 八小时的订单量矩阵。行代表工作日,列代表 14:00 至 21:00:
orders = np.array([
[12, 15, 18, 24, 31, 36, 28, 17],
[11, 14, 20, 27, 35, 39, 30, 19],
[13, 16, 21, 29, 38, 42, 32, 20],
[10, 13, 19, 26, 34, 37, 29, 18],
[14, 18, 23, 32, 41, 46, 35, 22],
])整数索引用坐标定位
逗号分隔沿各条轴的坐标:
tuesday_at_17 = orders[1, 3]
friday = orders[4]
last_hour = orders[:, -1]orders[1, 3] 选择一个标量。orders[4] 只提供第 0 轴坐标,因此保留整行。冒号表示“这条轴上的所有位置”。负数索引 -1 从末尾开始计数。
这些都是位置坐标,不是业务标签。NumPy 不知道第 1 行代表周二,也不知道第 3 列代表 17:00。标签必须单独保存;每当行列重排时,都要确认标签仍与数值对齐。
切片选择规则区间
切片(slice)采用 start:stop:step 形式:包含 start,不包含 stop,每次前进 step:
peak = orders[1:4, 3:7]
every_other_hour = orders[1:4, 3:7:2]第一个表达式选择第 1、2、3 行,以及第 3、4、5、6 列,结果形状为 (3, 4)。第二个表达式保留相同行,却只选择第 3、5 列,因此形状为 (3, 2)。
运行切片前,先预测坐标和输出形状。这个习惯比观察一张“看起来合理”的结果表更早发现轴颠倒和差一错误。
切片可能共享内存
NumPy 的基本切片经常返回视图(view),也就是引用同一底层数据、而不是复制数据的新数组。这种行为节省资源,但修改值时可能令人意外:
window = orders[1:3, 2:5]
window[0, 0] = -1
print(orders[1, 2]) # -1:原数组也改变了如果确实需要独立结果,应明确复制:
window_copy = orders[1:3, 2:5].copy()第 3 章将深入解释视图、副本和内存布局。目前只需记住:修改任何切片数组以前,都要明确决定是否允许原数据一起变化。
布尔掩码按条件选择
minutes <= 40 这类表达式会生成布尔数组,每个元素对应一个 True 或 False:
minutes = np.array([31.0, np.nan, 52.0, 68.0, 44.0])
within_target = minutes <= 40
print(within_target)
# [ True False False False False]把布尔数组放进方括号,会保留掩码为 True 的值:
selected = minutes[within_target]掩码必须与被筛选的数组或轴对齐。长度为 5 的掩码可以筛选长度为 5 的一维数组。对于二维数组,同形状的完整布尔掩码会选择匹配单元格,并且通常返回一维结果;一维行掩码则可以选择整行。
逐元素组合条件
真实筛选通常需要多个条件:
zones = np.array(["North", "South", "South", "South", "South"])
cancelled = np.array([False, False, False, False, True])
mask = (
~np.isnan(minutes)
& (minutes <= 60)
& (zones == "South")
& ~cancelled
)
selected_minutes = minutes[mask]使用 & 表示逐元素“且”,| 表示逐元素“或”,~ 表示逐元素取反。每个比较都应放在括号中,因为这些运算符与比较运算符的优先级不同。Python 的 and、or、not 要求一个真值;数组包含许多真值,所以 NumPy 会拒绝含义不明确的指令。
让掩码可以检查
很长的单行筛选可能正确,却难以审计。应为重要子条件命名:
has_time = ~np.isnan(minutes)
is_south = zones == "South"
is_active = ~cancelled
within_limit = minutes <= 60
analysis_mask = has_time & is_south & is_active & within_limit现在可以统计每项规则排除了多少记录:
print(np.count_nonzero(~has_time))
print(np.count_nonzero(analysis_mask))掩码属于证据边界的一部分。排除取消或缺失记录会改变总体和分母,最终报告必须说明这些排除条件。
筛选为计算准备好了输入。下一节会删除不必要的 Python 循环,并介绍广播:NumPy 根据形状组合数组、而不要求手工重复数据的规则。