Skip to content

第 8 章 布尔掩码与条件选择

学习目标

  • 理解布尔掩码索引的机制
  • 掌握用布尔数组过滤、统计
  • 掌握 np.where 的条件选择
  • 掌握逻辑运算 &|~any/all
  • 会用掩码做数据清洗

8.1 布尔掩码

布尔掩码(boolean mask)是与数组同形状的布尔数组,用 数组[掩码] 取出 True 对应的元素,False 对应的被过滤掉。

第 4 章学过,比较运算产生布尔数组;现在把它当索引用:

python
import numpy as np

scores = np.array([85, 92, 58, 90, 76, 95, 63])
mask = scores >= 90
print(mask)
print(scores[mask])

输出:

[False  True False  True False  True False]
[92 90 95]

maskTrue 的位置正好是 92、90、95 的位置,scores[mask] 只留下它们。

可以一步写成 scores[scores >= 90],不用中间变量。

8.2 统计满足条件的数量与比例

布尔数组可以直接求和、求平均:True 计为 1,False 计为 0。所以:

  • (条件).sum() 是满足条件的个数
  • (条件).mean() 是满足条件的比例
python
import numpy as np

scores = np.array([85, 92, 58, 90, 76, 95, 63])
print("90 分以上人数:", (scores >= 90).sum())
print("90 分以上比例:", (scores >= 90).mean())
print("不及格(<60)人数:", (scores < 60).sum())

输出:

90 分以上人数: 3
90 分以上比例: 0.42857142857142855
不及格(<60)人数: 1

8.3 np.where:条件选择

np.where(条件, 满足时的值, 不满足时的值) 逐元素二选一,返回与条件同形状的数组:

python
import numpy as np

a = np.array([10, 20, 30, 40])
print(np.where(a > 20, "高", "低"))
print(np.where(a > 20, 1, 0))

输出:

['低' '低' '高' '高']
[0 0 1 1]

两个值可以是标量,也可以是数组(此时按广播规则工作)。

只传一个参数时,np.where(条件) 返回满足条件的下标:

python
import numpy as np

a = np.array([10, 20, 30, 40])
print(np.where(a > 20))

输出:

(array([2, 3]),)

返回的是元组,里面每个元素对应一个轴的坐标。一维数组时取 np.where(a > 20)[0] 即可得到下标数组。

8.4 逻辑运算:& | ~

多个条件组合要用按位逻辑运算符(不是 Python 的 and/or):

  • &:与(两个都满足)
  • |:或(至少一个满足)
  • ~:非(取反)
python
import numpy as np

x = np.array([5, 12, 18, 25, 30])
print((x >= 10) & (x < 20))    # 10 ≤ x < 20
print((x < 10) | (x >= 25))    # 小于 10 或大于等于 25
print(~(x >= 10))              # 取反:小于 10

输出:

[False  True  True False False]
[ True False False  True  True]
[ True False False False False]

注意两个易错点:

  1. 不能用 and/or。它们会尝试把整个数组当作一个真值,报错:
    >>> x >= 10 and x < 20
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
  2. &>= 优先级低,所以复合条件必须加括号:(x >= 10) & (x < 20),漏括号会语法错误或逻辑错误。

8.5 any 与 all

把布尔数组整体压缩成一个真值:

  • (条件).any():只要有一个 True 就是 True
  • (条件).all():全部为 True 才是 True
python
import numpy as np

x = np.array([5, 12, 18, 25, 30])
print((x > 100).any())    # 有大于 100 的吗?没有
print((x > 0).all())      # 全部大于 0 吗?是

输出:

False
True

8.6 二维数组的掩码

二维数组用掩码过滤后变成一维——只留下 True 对应的元素,形状信息丢失:

python
import numpy as np

m = np.array([[1, 5, 3],
              [9, 2, 7]])
print(m[m > 4])
print(m[m > 4].mean())    # 大于 4 的元素平均值

输出:

[5 9 7]
7.0

8.7 应用:数据清洗

数据清洗是把脏数据(异常值、缺失值)找出来并处理的过程。两个经典操作:

操作一:把异常值替换为 nan。 温度数据里混入 99.9200.0 这类错误读数:

python
import numpy as np

temps = np.array([36.5, 37.0, 99.9, 36.8, 200.0, 36.6])
clean = np.where((temps > 40) | (temps < 35), np.nan, temps)
print(clean)

输出:

[36.5 37.   nan 36.8  nan 36.6]

操作二:排除缺失值后统计。np.isnan 找出 nan 并过滤:

python
import numpy as np

data = np.array([1.0, -999.0, 2.5, -999.0, 3.1])   # -999 表示缺失
data[data == -999.0] = np.nan                        # 统一标成 nan
print(data)
print("有效值平均:", data[~np.isnan(data)].mean())

输出:

[ 1.  nan  2.5 nan  3.1]
有效值平均: 2.1999999999999997

data == -999.0 是掩码,data[掩码] = np.nan 是「给掩码位置赋值」——第 3 章切片赋值的推广。

操作三:批量替换满足条件的元素。 把数组里所有偶数改成 0:

python
import numpy as np

m = np.array([[1, 2],
              [3, 4],
              [5, 6]])
m[m % 2 == 0] = 0
print(m)

输出:

[[1 0]
 [3 0]
 [5 0]]

动手实践

  1. 统计 np.array([85, 92, 58, 90, 76, 95, 63]) 中 80~90 分(含)的人数与比例(提示:两个条件用 & 连接)。
  2. np.wherenp.array([10, 20, 30, 40]) 中大于 25 的标记为 "大",其余为 "小"
  3. 造一组含 -999 缺失值的数组,替换为 nan 后求有效值平均。
  4. 生成 20 个随机整数(第 11 章预告:rng.integers(0, 100, 20)),用掩码取出所有偶数并求和。

常见错误

错误写法现象原因
(x >= 10) and (x < 20)ValueError: The truth value of an array ... is ambiguous布尔数组不能整体判断真值;用 &
x >= 10 & x < 20(漏括号)语法错误或错误结果& 优先级低于比较运算,必须加括号
m[m > 4].shape 想保持二维得到一维掩码过滤后形状会被压平,这是设计行为
np.where(条件) 当作返回下标数组直接用返回元组单参数形式返回各轴下标元组,取 [0] 得到一维下标
a[~np.isnan(a)] 时忘了 ~留下的是 nan~ 取反;直接 a[np.isnan(a)] 是留下缺失值
data[data == -999] = nan 对浮点数组正常注意:对 nan 做 == 比较永远 False,所以清洗时要先替换成 nan 再判断

章末练习

基础

  1. 用掩码从 np.array([3, 8, 15, 22, 7]) 取出大于 10 的元素。
  2. 统计 np.array([1, 2, 3, 4, 5, 6]) 中偶数的个数(提示:% 2 == 0)。
  3. np.wherenp.array([-2, 5, -8, 9]) 中负数改成 0,正数保持不变。

提高

  1. 统计 np.array([72, 85, 58, 90, 66, 95, 63, 88]) 中 60~85 分(含)的人数,并打印这些分数。
  2. 对一组含异常值的数据(自行设计,如身高数据混入 200),用掩码把异常值替换为 nan,再求有效值平均与标准差。

挑战

  1. np.where 实现分段函数:当 x < 0 时取 0,0 ≤ x < 10 时取 1,x ≥ 10 时取 2(提示:可以嵌套 np.where 或多次赋值)。
  2. 给定 np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]]),不用循环找出所有大于列平均的元素,并统计个数(提示:先算列平均,再广播比较)。

章末自测

每题选择一个最佳答案。本书不附答案:完成后交由老师或 AI 老师批改讲解。

  1. a = np.array([1, 2, 3, 4]); a[a > 2] 的结果是?
    • A. [3, 4]
    • B. [2, 3, 4]
    • C. [1, 2]
    • D. 报错
  2. (a > 2).sum() 表示?
    • A. 大于 2 的元素个数
    • B. 大于 2 的元素之和
    • C. 所有元素之和
    • D. 数组长度
  3. 组合两个条件「大于 10 且小于 20」的正确写法是?
    • A. a > 10 and a < 20
    • B. (a > 10) & (a < 20)
    • C. a > 10 & a < 20
    • D. and(a > 10, a < 20)
  4. np.where(a > 2, "高", "低")[1, 3, 2] 的结果是?
    • A. ['低', '高', '高']
    • B. ['低', '高', '低']
    • C. ['高', '低', '高']
    • D. [1, 3, 2]
  5. (a > 100).any()[1, 50, 200] 的结果是?
    • A. True
    • B. False
    • C. [True]
    • D. 报错
  6. 布尔掩码索引 m[m > 4](m 是二维)的结果是?
    • A. 二维数组
    • B. 一维数组
    • C. 布尔数组
    • D. 报错
  7. 统计满足条件的比例,应使用?
    • A. (条件).sum()
    • B. (条件).mean()
    • C. (条件).max()
    • D. (条件).size
  8. 取反操作符是?
    • A. !
    • B. ~
    • C. not
    • D. -
  9. 把缺失值标记为 nan 后,排除它统计的写法是?
    • A. data[~np.isnan(data)]
    • B. data[np.isnan(data)]
    • C. data[data == np.nan]
    • D. np.filter(data)
  10. np.where(a > 2)(只有一个参数)返回的是?
    • A. 满足条件的元素
    • B. 满足条件的下标(元组)
    • C. 布尔数组
    • D. 报错