欢迎光临
我们一直在努力

Python NumPy — 数组的比较运算,生成布尔类型数组

在这里插入图片描述

👋 大家好,欢迎来到我的技术博客! 📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。 🎯 本文将围绕NumPy这个话题展开,希望能为你带来一些启发或实用的参考。 🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!


文章目录

  • Python NumPy — 数组的比较运算,生成布尔类型数组
    • 一个绕不开的开头
    • 标量也能比
    • 完整的比较运算符家族
    • 组合比较:and / or 在这里是陷阱
    • 布尔数组到底能拿来干嘛
      • 1. 统计满足条件的元素个数
      • 2. 判断有没有、是不是全满足
      • 3. 布尔索引:最常用的用法
      • 4. 按条件替换:where 的两种用法
      • 5. 找出 True 的坐标:nonzero / argwhere / flatnonzero
    • 浮点数的比较,别用 ==
    • 整体比较:array_equal 才是"两个数组相等"
    • 逐轴比较
    • 字符串也能比
    • 类型转换的坑
    • 一份实战代码:不合格订单清洗
    • 一张流程图,把整条链路串起来
    • 再补两个容易忽略的细节
    • 性能心法:向量化 vs 循环
    • 收个尾

Python NumPy — 数组的比较运算,生成布尔类型数组


上个月我在线上排查一个推荐系统的过滤逻辑,卡了整整一下午。问题出在一行我三年前写的代码上:

mask = df[df['score'] >= threshold]

这行本身没错。错的是我把它套进了一个循环里,每次迭代都在堆栈里复制一份布尔序列。数据量 800 万行的时候,内存直接飙到 40 多 GB,Pod 被 OOM Killer 干掉了。重启之后我盯着监控曲线,人傻了。

那次排查让我重新把布尔数组这件事从头到尾捋了一遍。这篇文章就是我捋出来的东西,包括那些当时查文档都查不清楚的边角。

一个绕不开的开头

先抛个问题。

import numpy as np

a = np.array([1, 2, 3, 4])
b = np.array([1, 3, 2, 4])

print(a == b)

很多人会下意识觉得,这应该返回 True 或者 False,一个标量。毕竟两个数组"相不相等"嘛。

实际输出长这样:

[ True False False True]

返回的是一个数组,dtype=bool。这才是 NumPy 比较运算的真正核心:它不做"整体是否相等"的判断,而是把比较这个动作广播到每一个元素上,逐个元素去比,返回一堆布尔值。

这个语义差异,是我见过最容易被误解的 NumPy 概念之一,没有之一。它和 Python 内建列表的行为完全不一样:

x = [1, 2, 3]
y = [1, 4, 3]
print(x == y) # False,比较的是整个列表对象

同一个 == 号,在列表和数组里是两个世界的东西。搞混了,后面全是坑。

#mermaid-svg-kb15iGDmmo4dBXql{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kb15iGDmmo4dBXql .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kb15iGDmmo4dBXql .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kb15iGDmmo4dBXql .error-icon{fill:#552222;}#mermaid-svg-kb15iGDmmo4dBXql .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kb15iGDmmo4dBXql .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kb15iGDmmo4dBXql .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kb15iGDmmo4dBXql .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kb15iGDmmo4dBXql .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kb15iGDmmo4dBXql .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kb15iGDmmo4dBXql .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kb15iGDmmo4dBXql .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kb15iGDmmo4dBXql .marker.cross{stroke:#333333;}#mermaid-svg-kb15iGDmmo4dBXql svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kb15iGDmmo4dBXql p{margin:0;}#mermaid-svg-kb15iGDmmo4dBXql .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-kb15iGDmmo4dBXql .cluster-label text{fill:#333;}#mermaid-svg-kb15iGDmmo4dBXql .cluster-label span{color:#333;}#mermaid-svg-kb15iGDmmo4dBXql .cluster-label span p{background-color:transparent;}#mermaid-svg-kb15iGDmmo4dBXql .label text,#mermaid-svg-kb15iGDmmo4dBXql span{fill:#333;color:#333;}#mermaid-svg-kb15iGDmmo4dBXql .node rect,#mermaid-svg-kb15iGDmmo4dBXql .node circle,#mermaid-svg-kb15iGDmmo4dBXql .node ellipse,#mermaid-svg-kb15iGDmmo4dBXql .node polygon,#mermaid-svg-kb15iGDmmo4dBXql .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kb15iGDmmo4dBXql .rough-node .label text,#mermaid-svg-kb15iGDmmo4dBXql .node .label text,#mermaid-svg-kb15iGDmmo4dBXql .image-shape .label,#mermaid-svg-kb15iGDmmo4dBXql .icon-shape .label{text-anchor:middle;}#mermaid-svg-kb15iGDmmo4dBXql .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kb15iGDmmo4dBXql .rough-node .label,#mermaid-svg-kb15iGDmmo4dBXql .node .label,#mermaid-svg-kb15iGDmmo4dBXql .image-shape .label,#mermaid-svg-kb15iGDmmo4dBXql .icon-shape .label{text-align:center;}#mermaid-svg-kb15iGDmmo4dBXql .node.clickable{cursor:pointer;}#mermaid-svg-kb15iGDmmo4dBXql .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kb15iGDmmo4dBXql .arrowheadPath{fill:#333333;}#mermaid-svg-kb15iGDmmo4dBXql .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kb15iGDmmo4dBXql .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kb15iGDmmo4dBXql .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kb15iGDmmo4dBXql .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kb15iGDmmo4dBXql .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kb15iGDmmo4dBXql .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kb15iGDmmo4dBXql .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kb15iGDmmo4dBXql .cluster text{fill:#333;}#mermaid-svg-kb15iGDmmo4dBXql .cluster span{color:#333;}#mermaid-svg-kb15iGDmmo4dBXql div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kb15iGDmmo4dBXql .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kb15iGDmmo4dBXql rect.text{fill:none;stroke-width:0;}#mermaid-svg-kb15iGDmmo4dBXql .icon-shape,#mermaid-svg-kb15iGDmmo4dBXql .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kb15iGDmmo4dBXql .icon-shape p,#mermaid-svg-kb15iGDmmo4dBXql .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kb15iGDmmo4dBXql .icon-shape .label rect,#mermaid-svg-kb15iGDmmo4dBXql .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kb15iGDmmo4dBXql .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kb15iGDmmo4dBXql .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kb15iGDmmo4dBXql :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

a = np.array([1,2,3,4])

a == b

b = np.array([1,3,2,4])

逐元素比较

array([True, False, False, True])

dtype=bool

可继续参与运算 / 过滤 / 统计

标量也能比

数组不只能和数组比,还能和标量比。这个叫广播,但比较场景里它简单得让人忽略它。

a = np.array([1, 2, 3, 4, 5])

print(a > 3)
# [False False False True True]

标量 3 被自动扩展成和 a 一样长的数组,再逐个比。心里要有这个概念:a > 3 不是拿一个数组去和一个数字"整体"比,是拿每个元素去和 3 比。

广播的完整规则以后单独写。这里只记住一句:形状相同能比,形状能对齐也能比,标量永远能比。

col = np.array([[1], [2], [3]]) # 形状 (3, 1)
row = np.array([10, 20]) # 形状 (2,)

print(col > row)

输出:

[[False False]
[False False]
[False False]]

两个形状对不齐的数组,一个 (3,1) 一个 (2,),广播之后对齐成 (3,2) 再逐元素比。这里全是 False,因为左边的值都小。广播在比较运算里同样生效,这是做二维表格比较的基础。

完整的比较运算符家族

比较运算有对应的 ufunc。运算符和函数名一一对应:

运算符ufunc 函数含义
== np.equal 等于
!= np.not_equal 不等于
< np.less 小于
<= np.less_equal 小于等于
> np.greater 大于
>= np.greater_equal 大于等于

两者结果完全一致:

a = np.array([1, 2, 3])
b = np.array([3, 2, 1])

print(a > b) # [False False True]
print(np.greater(a, b)) # [False False True]

用哪个纯粹是风格问题。我个人的习惯是,长度可控、手写的一次性代码里用运算符,因为读起来直接;写在库函数里、或者要动态拼比较条件的时候,用 ufunc 形式,因为可以把函数本身当参数传:

ops = {
"gt": np.greater,
"ge": np.greater_equal,
"lt": np.less,
"le": np.less_equal,
"eq": np.equal,
"ne": np.not_equal,
}

说实话,运算符你没法放进字典里当值用,ufunc 能。这个区别在写可配置的过滤框架时是刚需。

顺带一提,NumPy 也有 Python 风格的原地比较,比如 np.less_equal 对应的原地操作没必要在这里展开。运算符这条路径,能覆盖 99% 的实际场景。

组合比较:and / or 在这里是陷阱

这是我重点想聊的一处。初学者的第一反应会写出这种代码:

import numpy as np

a = np.array([1, 2, 3, 4])

# 想取"大于1且小于4"的元素
mask = a > 1 and a < 4 # 直接报错

报什么错?

ValueError: The truth value of an array with more than one element is ambiguous.
Use a.any() or a.all()

这句话我建议你直接背下来,因为你会反复遇到它。“多元素数组的真值有歧义,请用 any() 或 all()”。

为什么报错?因为 and、or 是 Python 的短路逻辑运算符,它们会去调用对象的布尔真值来决定短路。可一个含有多个元素的布尔数组,它的"真假"到底算真还是算假?没法定义,所以 NumPy 直接抛异常,把决定权扔回给你。

正确做法是用位运算符,或者用 np.logical_* 系列函数:

a = np.array([1, 2, 3, 4])

mask1 = (a > 1) & (a < 4)
mask2 = np.logical_and(a > 1, a < 4)

print(mask1) # [False True True False]
print(mask2) # [False True True False]

& 和 | 是逐元素的,正好和布尔数组的逐元素语义对上了。and 和 or 是整体短路的,语义根本不对。这就是为什么 NumPy 里要用位运算来做逻辑组合。

⚠️ 有个坑必须点名:括号不能省。

mask = a > 1 & a < 4 # 语法上能过,语义上错了

这里 & 比 >、< 的优先级更高,实际被解析成 a > (1 & a) < 4,结果全错,而且不报错,就这么静默地给你一堆垃圾值。这种 bug 最恶心,因为它合法、能跑、结果还看着像那么回事。所以我写组合比较,括号永远加满。

np.logical_* 家族有四个成员:

函数运算符含义
np.logical_and & 逻辑与
np.logical_or | 逻辑或
np.logical_xor ^ 逻辑异或
np.logical_not ~ 逻辑非

异或的场景相对少,但"非"用得非常多:

a = np.array([1, 2, 3, 4])

not_one = ~(a == 1)
not_one2 = np.logical_not(a == 1)

print(not_one) # [False True True True]
print(not_one2) # [False True True True]

~ 是取反。要命的细节又来了:not 这个 Python 关键字在 NumPy 里不能用,因为 not 也是整体布尔判断,和 and、or 一样会掉进"真值歧义"的坑。

mask = not (a == 1) # 又是那个 ValueError

记住:逻辑组合用 & | ~ ^,永远不用 and or not。这条规则记住了,能躲掉 NumPy 里一多半的初学者报错。

布尔数组到底能拿来干嘛

到这里你可能觉得,就一个 True/False 数组,能玩出什么花。它花活多得很。布尔数组在 NumPy 里的地位,相当于 SQL 里的 WHERE 子句。

1. 统计满足条件的元素个数

rng = np.random.default_rng(42)
data = rng.normal(0, 1, size=10_000)

# 有多少个元素大于 0
positive = (data > 0).sum()
print(positive)

True 本质上是 1,False 是 0,所以 .sum() 直接数出 True 的个数。这是最简洁的统计方式。np.count_nonzero 是等价的显式写法:

print(np.count_nonzero(data > 0))

两个结果一模一样。我偏爱 .sum(),手少打几个字符,但 np.count_nonzero 语义更直白——它不依赖"布尔当整数用"这个隐式约定。

2. 判断有没有、是不是全满足

a = np.array([1, 2, 3, 4, 5])

print((a > 3).any()) # True,存在大于3的
print((a > 3).all()) # False,不是全都大于3

any() 是"至少有一个真",all() 是"全部为真"。这俩在写断言、写提前返回的时候天天用。

3. 布尔索引:最常用的用法

scores = np.array([88, 92, 61, 75, 96, 54])

passed = scores[scores >= 60]
print(passed) # [88 92 75 96]

把布尔数组放进方括号里,NumPy 就只挑出 True 对应的元素。这叫布尔索引(boolean indexing),也叫 fancy indexing 的一种。这是 pandas 里 df[df['col'] > x] 那套写法的底层来源。

注意一点:布尔索引返回的是副本,不是视图。对结果做修改,不会影响原数组:

scores = np.array([88, 92, 61])
passed = scores[scores >= 60]
passed[0] = 999
print(scores) # [88 92 61],原数组没变

这个"返回副本"的语义很重要。写代码的时候如果不记得,会踩"改了没生效"或者"以为会改原数组"的坑。

4. 按条件替换:where 的两种用法

np.where 用得多,也容易被用混。它有两种形态。

形态一,只有一个条件参数,返回满足条件的索引:

a = np.array([1, 2, 3, 4, 5])

idx = np.where(a > 3)
print(idx) # (array([3, 4]),)

print(a[idx]) # [4 5]

注意返回的是个元组,即使是一维数组也包了一层。因为 np.where 是为了二维、高维数组设计的,每个轴返回一个索引数组。

形态二,三个参数,做条件替换:

a = np.array([1, 2, 3, 4, 5])

result = np.where(a > 3, a, 0)
print(result) # [0 0 0 4 5]

等价于:a > 3 的地方取 a 本身,否则取 0。这是 NumPy 版的"三元表达式",在特征工程里清洗异常值常用。

两种形态不要搞混。一个参数给索引,三个参数给替换值。中间态(两个参数)不存在,会报错。

5. 找出 True 的坐标:nonzero / argwhere / flatnonzero

布尔数组拿到手之后,常要把 True 的位置挖出来。三个函数干的是类似的活,但有细微差别。

x = np.arange(6).reshape(2, 3)
# [[0 1 2]
# [3 4 5]]

m = (x % 2 == 0)
print(m)
# [[ True False True]
# [False True False]]

print(np.nonzero(m))
# (array([0, 0, 1]), array([0, 2, 1]))

np.nonzero 返回一个元组,每个轴一个索引数组。第一个 [0,0,1] 是行,第二个 [0,2,1] 是列,两两配对读,就是 (0,0)、(0,2)、(1,1) 三个 True 的位置。

np.argwhere 把它打包成更直观的形式:

print(np.argwhere(m))
# [[0 0]
# [0 2]
# [1 1]]

每行一个坐标。np.argwhere(m) 本质上等价于把 np.nonzero(m) 的结果转置再堆叠。日常读坐标,argwhere 更顺眼。

np.flatnonzero 只适用于一维:

flat = np.flatnonzero(x > 1)
print(flat) # [2 3 4 5]

它把数组"拉平"之后返回非零(True)位置的扁平索引。和 np.nonzero(x.ravel() > 1)[0] 一个意思。选哪个看你后面要拿索引干嘛。

浮点数的比较,别用 ==

这节是能救命的。新手特别容易在这里栽跟头。

a = 0.1 + 0.2
print(a == 0.3) # False

为什么?0.1 和 0.2 在二进制里是无限循环小数,存进浮点数就被截断了,加出来的结果和 0.3 的浮点表示差那么一丁点。这点误差在 NumPy 的大数组里到处都在累积。

正确的做法是用 np.isclose 或者 np.allclose:

import numpy as np

a = np.array([0.1, 0.2, 0.3])
b = np.array([0.3, 0.1, 0.1 + 0.2])

print(np.isclose(a, b))
# [False False True]

np.isclose 默认的相对容差是 rtol=1e-05,绝对容差是 atol=1e-08。判断依据是 |a – b| <= atol + rtol * |b|。这个公式和 Python 标准库 math.isclose 保持一致,只是 np.isclose 支持数组。

要判断整个数组是否近似相等,用 np.allclose,它等价于 np.all(np.isclose(a, b)):

a = np.array([1.0, 2.0])
b = np.array([1.0, 2.0])

print(np.allclose(a, b)) # True

什么场景必须用这个?一堆场景。两个矩阵经过不同路径算出来,理论上应该相等,浮点误差让它们差了 1e-15。你拿 == 去比,结论是"不等",拿 isclose 比,才是"算对了"。做矩阵求逆、做归一化、做梯度检查的时候,== 就是灾难。

NaN 是个更坑的存在。 NaN 和任何数比,结果都是 False,包括和它自己比:

import numpy as np

arr = np.array([1.0, np.nan, 2.0])

print(arr == np.nan) # [False False False],连 NaN 自己都不等于 NaN
print(np.isnan(arr)) # [False True False]

这就是为什么判断 NaN 必须用 np.isnan,而不是 ==。当年我在一个数据清洗脚本里用了 arr[arr == np.nan] 想过滤 NaN,结果一个都没过滤掉,脏数据顺着管道一路冲到下游,线上指标异常了一晚上才发现。这玩意儿真的离谱。

顺带的坑:np.array_equal 对 NaN 也"不友好"——两个都含 NaN 的数组,会被判成不相等:

a = np.array([1.0, np.nan])
b = np.array([1.0, np.nan])

print(np.array_equal(a, b)) # False

如果你要的是"含 NaN 也算相等"的语义,得自己处理:

def nan_aware_equal(a, b):
return np.array_equal(a, b, equal_nan=True)

np.array_equal 有 equal_nan 参数,设为 True 时,两个对应位置都是 NaN 会被视为相等。这个参数是较新版本加的,具体从哪个版本开始支持,各版本有差异,老环境的脚本里最好别依赖它,要对照自己环境的 NumPy 版本确认。

整体比较:array_equal 才是"两个数组相等"

绕回开头那个问题。既然 a == b 返回的是逐元素布尔数组,那"判断两个数组整体是否相等"改怎么办?

np.array_equal:

a = np.array([1, 2, 3])
b = np.array([1, 2, 3])
c = np.array([1, 2, 4])

print(np.array_equal(a, b)) # True
print(np.array_equal(a, c)) # False

它做了两件事:形状一致,且所有对应元素(按 == 或 equal_nan)都相等。所以严格说,"两个数组相等"的判据不是 ==,而是 np.array_equal。

还有个 np.array_equiv,历史遗留函数,行为上和 array_equal 有微妙差别(比如对形状不同的数组会尝试广播)。日常我基本不用它,array_equal 更直观。

逐轴比较

布尔数组是带维度的,很多操作可以指定 axis。

a = np.array([[1, 2, 3],
[4, 5, 6]])

big = a > 3
print(big)
# [[False False False]
# [ True True True]]

print(big.any(axis=0)) # [ True True True],每列是否至少有一个True
print(big.any(axis=1)) # [False True],每行是否至少有一个True
print(big.all(axis=0)) # [False False False]
print(big.all(axis=1)) # [False True]

axis=0 是"沿着第 0 轴往下压",对列操作,返回每列的结果。axis=1 对行操作。这个轴的方向,是 NumPy 里最折磨人的心智负担之一,多写多错,错了就回来对一遍形状。

np.where 也支持逐轴拿到坐标,前面已经展示过二维的例子,这里不再重复。

字符串也能比

布尔比较不止数字。NumPy 的字符串数组(dtype='<U',即 Unicode 字符串)支持直接比较:

names = np.array(['alice', 'bob', 'carol'])

print(names == 'bob') # [False True False]
print(names >= 'bob') # [False True True]

但这套比较是字典序的,按码点逐字符比,不是按长度,也不是按你脑子里的"自然序"。所以 'carol' >= 'bob' 是 True,因为首字符 'c' > 'b'。

顺带提醒:NumPy 里还有 np.char 模块,提供 np.char.startswith、np.char.endswith 这些向量化的字符串操作,它们比手写循环快得多。这些能返回布尔数组,可以和比较运算组合用。不过 np.char 的字符串处理能力远不如 pandas 的 .str 访问器,具体用哪个看你在什么栈里。

类型转换的坑

布尔数组是从比较里"派生"出来的,但它也有自己的数据类型问题。

a = np.array([1, 2, 3], dtype='int8')
b = np.array([1.0, 2.0, 3.0], dtype='float64')

print(a == b)
# [ True True True]

不同 dtype 之间比较,NumPy 会先做类型提升(type promotion),对齐到一个公共类型再比。int8 和 float64 比,会提升到 float64。这个行为多数时候符合直觉,但遇到大整数和浮点混用时要留个心眼——精度会丢。

一份实战代码:不合格订单清洗

把上面这些串起来,写一个贴近真实工作的例子。电商后端常见的一个需求:从订单金额数组里,筛出"异常订单"。规则是——金额非正、或者金额是 NaN、或者金额超过某个合理上限,都算异常。

import numpy as np

# 示意代码,具体业务阈值以实际为准
amounts = np.array([120.0, 5.0, 300.0, np.nan, 45.0, 999999.0])

valid_positive = amounts > 0 # 金额必须为正
not_nan = ~np.isnan(amounts) # 不能是 NaN
within_limit = amounts < 100_000.0 # 有上限

valid = valid_positive & not_nan & within_limit

print("有效订单金额:", amounts[valid])
print("异常订单个数:", np.count_nonzero(~valid))
print("异常订单下标:", np.flatnonzero(~valid))

输出:

有效订单金额: [120. 300. 45.]
异常订单个数: 3
异常订单下标: [1 3 5]

关键点解释:

  • 三个布尔条件用 & 组合,不用 and。
  • 判断 NaN 用 np.isnan,不碰 ==。
  • 用 ~valid 取反得到异常订单,而不是重新写一遍反向条件。
  • np.flatnonzero 拿到异常位置的下标,方便回查原始记录。

这里如果写成 amounts > 0 and …,立刻就是那个 ValueError;如果写成 amounts == np.nan 想抓 NaN,一个都抓不到。两个坑在前面都埋过伏笔,这里一起现形。

一张流程图,把整条链路串起来

渲染错误: Mermaid 渲染失败: Parse error on line 3: …["== != < <= > >="/"np.equal 等 ufunc"] ———————–^ Expecting 'SQE', 'DOUBLECIRCLEEND', 'PE', '-)', 'STADIUMEND', 'SUBROUTINEEND', 'PIPE', 'CYLINDEREND', 'DIAMOND_STOP', 'TAGEND', 'TRAPEND', 'INVTRAPEND', 'UNICODE_TEXT', 'TEXT', 'TAGSTART', got 'STR'

这张图建议收藏。你写布尔逻辑卡壳的时候,顺着箭头走一遍,就知道卡在哪一步了。

再补两个容易忽略的细节

细节一,> 链式比较是坑。 Python 里 1 < x < 3 是合法的链式比较,等价于 1 < x and x < 3。但 NumPy 里这套行不通:

a = np.array([1, 2, 3, 4])

# 想写 1 < a < 3 这种链式,结果是错的
mask = 1 < a < 3 # 会被解析成 (1 < a) and (a < 3),触发真值歧义

因为 and 会触发真值歧义的 ValueError,所以链式比较在 NumPy 数组上直接阵亡。老老实实写 (a > 1) & (a < 3)。

细节二,布尔数组做索引,长度必须匹配。 布尔索引要求掩码的长度和被索引轴的长度一致,否则报 IndexError。这点和整数索引不同,整数索引可以越界报错,布尔索引是长度不匹配报错,两码事。

性能心法:向量化 vs 循环

回到开头那个 OOM 事故。我当时的循环,本质上是在用 Python 级别的 for 反复做比较、反复复制布尔数组。而 NumPy 的比较是 C 级别向量化的,一个 > 糊在整个数组上,内部并行,快一个数量级起步。

import numpy as np
import time

data = np.random.default_rng(1).normal(0, 1, 5_000_000)

# 向量化:一趟搞定
t0 = time.perf_counter()
mask = data > 0
cnt = mask.sum()
t1 = time.perf_counter()
print(f"向量化: {t1 t0:.4f}s, 计数={cnt}")

# 反例:手写循环
t0 = time.perf_counter()
cnt = 0
for x in data:
if x > 0:
cnt += 1
t1 = time.perf_counter()
print(f"循环: {t1 t0:.4f}s, 计数={cnt}")

在我这台机器上,向量化版本比 for 循环快了几十倍到上百倍。这个差距不是小数级别,是数量级级别。所以铁律是:能向量化就不写循环,能用布尔数组就不用 Python 的 if 分支。

如果你想更系统地了解布尔索引和 fancy indexing 的底层行为,官方文档的两页值得翻:一页是 Indexing on ndarrays,讲各种索引语义;一页是 Boolean array indexing,专门讲布尔掩码。还有 numpy.isclose 的 API 文档,想拿捏容差参数就把这页吃透。

这几页都是 NumPy 官方站点,稳定可访问。具体 API 的行为细节,永远以官方文档为准——我写出来的这些,是基于当前我在用的版本(2.2.6)验证过的,跨版本细节差异请对照自己环境的 changelog。

收个尾

布尔数组这件事,拆开看没什么高深的,全是 True 和 False。难的是把这些零散的语义——逐元素比较、位运算组合、NaN、浮点容差、布尔索引、逐轴归约——在脑子里拼成一整块,然后在写代码的时候不假思索地用对。

我踩过的坑,浓缩起来就三句:

  • 组合条件用 & | ~,碰 and or not 就是 ValueError。
  • 浮点比等于用 isclose,NaN 用 isnan,别用 ==。
  • 布尔索引返回副本,改了不碰原数组。
  • 把这三句贴在键盘边上,能省掉大半的排查时间。剩下的,就是多写,写到"布尔数组是 WHERE 子句"这个念头变成肌肉记忆为止。


    🙌 感谢你读到这里! 🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。 💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友! 💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿 🔔 关注我,不错过下一篇干货!我们下期再见!✨

    赞(0)
    未经允许不得转载:171主机测评 » Python NumPy — 数组的比较运算,生成布尔类型数组
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址