4.2 花式索引与布尔掩码的拷贝陷阱


文档摘要

4.2 花式索引与布尔掩码的拷贝陷阱 本节摘要:用下标数组取值(花式索引)和用布尔条件取值(掩码)都返回拷贝——因为选中的元素在内存里不再等距,无法用一套 strides 描述。代价是与元素数成正比的搬运;好处是改动结果绝不影响原数组。本节做对照实验,讲掩码三连(取、改、数)与组合条件的短路写法。 为什么它们必须是拷贝 基本切片能做视图,是因为选中元素仍呈"等差排列",一套新的起点加步长就能描述。花式索引选中的下标是任意的: 布尔掩码同理——条件哪些位置为真完全取决于数据本身: 三种取数方式的代价对照 三种取数方式的代价对照 掩码三连:取、改、数 掩码真正的威力在"就地条件修改": 注意第二连的细节:掩码在赋值右侧产生拷贝,出现在赋值左侧则变成"写入哪些位置"的指示器,直接改原数组。

4.2 花式索引与布尔掩码的拷贝陷阱

本节摘要:用下标数组取值(花式索引)和用布尔条件取值(掩码)都返回拷贝——因为选中的元素在内存里不再等距,无法用一套 strides 描述。代价是与元素数成正比的搬运;好处是改动结果绝不影响原数组。本节做对照实验,讲掩码三连(取、改、数)与组合条件的短路写法。

为什么它们必须是拷贝

基本切片能做视图,是因为选中元素仍呈"等差排列",一套新的起点加步长就能描述。花式索引选中的下标是任意的:

import numpy as np a = np.arange(10) picked = a[[7, 2, 8, 2]] # 顺序任意,还能重复 print(picked) # [7 2 8 2] print(np.shares_memory(picked, a)) # False —— 拷贝 # 元素 7、2、8 在原块里字节间距毫无规律,没有任何 strides 能同时描述 # 唯一办法:把字节逐个搬到一块新内存

布尔掩码同理——条件哪些位置为真完全取决于数据本身:

scores = np.array([88, 45, 92, 59, 73]) mask = scores >= 60 print(mask) # [True False True False True] passed = scores[mask] print(passed) # [88 92 73] print(np.shares_memory(passed, scores)) # False

三种取数方式的代价对照

三种取数方式的代价对照

掩码三连:取、改、数

掩码真正的威力在"就地条件修改":

import numpy as np signal = np.array([0.5, -3.2, 1.1, -0.4, 2.7]) # 一连:取(拷贝) print(signal[signal > 0]) # [0.5 1.1 2.7] # 二连:改(对原数组条件写入!掩码在赋值左侧是另一回事) signal[signal < 0] = 0.0 # 负半轴归零 print(signal) # [0.5 0. 1.1 0. 2.7] # 三连:数 rng = np.random.default_rng(0) data = rng.normal(size=1000) print("超过两倍标准差的点:", (np.abs(data) > 2).sum()) # 输出示例:43 print("占比:", (np.abs(data) > 2).mean().round(3)) # 0.043

注意第二连的细节:掩码在赋值右侧产生拷贝,出现在赋值左侧则变成"写入哪些位置"的指示器,直接改原数组。两个角色,一个语法位置决定。

组合条件必须用圆括号括起来,且 & 不能写成 and:

import numpy as np x = np.arange(10) both = x[(x > 2) & (x < 7)] print(both) # [3 4 5 6] # x > 2 and x < 7 会抛 ValueError: # 数组的真值判断是歧义的,一整列 True False 无法当单个布尔用 either = x[(x < 2) | (x > 7)] print(either) # [0 1 8 9] # 取反用 ~ print(x[~(x % 2 == 0)]) # [1 3 5 7 9]

案例:传感器数据清洗

完整过程。背景:一万点温度读数,混有传感器故障产生的 -999 占位与偶发尖峰,要清洗并统计。

操作:

import numpy as np rng = np.random.default_rng(3) temp = rng.normal(25, 1.5, size=10000) temp[rng.random(10000) < 0.01] = -999.0 # 撒1%故障占位 temp[rng.random(10000) < 0.005] += 30.0 # 撒0.5%尖峰 valid = (temp > -100) & (temp < 45) # 有效条件 print("有效点数:", valid.sum()) # 输出示例:9458 clean = temp[valid] print("清洗后均值:", clean.mean().round(3)) # 输出示例:25.31 print("清洗后标准差:", clean.std().round(3)) # 输出示例:1.503 # 剔除尖峰的变式:用统计量自适应阈值,而不是硬编码45度 z = np.abs((temp - np.median(temp)) / temp.std()) adaptive = temp[z < 4] print("自适应清洗后:", adaptive.mean().round(3)) # 输出示例:25.15

结果与解读:硬阈值清掉约 542 个坏点,均值回到 25.3 附近;自适应版本用中位数抗污染(尖峰拉不动中位数),效果更稳。变式:若要把坏点位置补成插值,先 np.where(valid, temp, np.nan) 转缺失,再用 nan 前向填充函数处理。

⚠️ 常见坑:链式索引 a[mask][1] = 0 不改原数组——a[mask] 是拷贝,后续赋值落在拷贝上然后被丢弃。条件写入必须一步到位:a[mask] = 0。

花式索引的多维配对语法

两个花式下标数组同时出现在不同维度时,含义从"分别取行列"变成"配对取点",这是高级索引里最反直觉的一处:

import numpy as np m = np.arange(12).reshape(3, 4) print(m[[0, 1], [2, 3]]) # 取 (0,2) 和 (1,3) 两个点,配对! # [2 7] # 想要"0、1行 与 2、3列"的整块子矩阵,要用交叉语法: block = m[np.ix_([0, 1], [2, 3])] print(block) # [[2 3] # [6 7]] # 或者其中一维退化为基本切片: print(m[[0, 1], 2:4]) # 行用花式、列用切片,得 2x2 块 # [[2 3] # [6 7]]

规则一句话:花式索引在不同维度上"配对生成坐标点",基本切片与花式索引混用时,花式的那一维定行、切片的那一维保持区间。想要笛卡尔积式的子块,np.ix_ 是显式声明。这个语法冷门但一错就错得离谱,见到 m[[...],[...]] 的代码多看一眼配对含义。

本节要点回顾

  • 拷贝的必然性:下标无规律则无 strides 可描述,只能逐元素搬运
  • 掩码双角色:右侧取值是拷贝,左侧赋值是条件写入原数组
  • 组合条件:括号加 & | ~,and/or 对数组直接报错
  • 链式索引失效:先取掩码再赋值等于改一次性拷贝,务必单步完成
  • 清洗套路:构造条件、sum 数量、布尔取值、where 兜底,四件套覆盖九成过滤需求

下一节回答"那到底还要不要循环":Python 层遍历的真实代价,以及确实需要元素级流程时的 nditer 正确姿势。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U