本小节将介绍使用布尔遮盖(掩码)来测试和操作NumPy数组的知识。当我们想通过一些标准对数组中的元素值进行提取、修改、计数或者其他一些操作的时候,我们需要使用遮盖:例如,你需要计算所有大于某个特定值的元素个数,或者删除那些超出阈值的离群值。在NumPy当中,布尔遮盖基本上是实现这类任务的最有效方式。
例子:计算下雨的天数
设想你有一系列数据代表着某个城市一年中每天的降水量。例如,下面我们将使用Pandas读取2014年西雅图的每天降雨统计数据(Pandas我们将在第三章详细介绍):
import numpy as np
import pandas as pd
# 使用Pandas读取降水量以英寸为单位的数据
rainfall = pd.read_csv('../data/Seattle2014.csv')['PRCP'].values
inches = rainfall / 254.0 # 0.1毫米转换成英寸
inches.shape
(365,)
这个数组包含着365个元素值,这些值代表着西雅图市2014年从1月1日到12月31日的降雨(单位英寸)。
我们使用图表可视化展示一下,用简单的直方图来画出降雨天数的分布情况。这里需要使用到Matplotlib(有关内容我们将在第四章详细介绍):
%matplotlib inline
import matplotlib.pyplot as plt
import seaborn; seaborn.set() # 设置图表的风格,seaborn
plt.hist(inches, 40); # 将降水量区间40等分作为横轴,将落在区间的元素个数作为纵轴
上面的直方图给我们提供了一个对这个数据集的通用观察结论:虽然名声在外,但事实上西雅图在2014年中绝大部分日子的降雨量都接近于0。但是这张图并没有帮助我们了解一些我们希望得到的数据:例如,一年之中有多少天在下雨?下雨的日子中降水量的平均值是多少?一年之中有多少天降水量超过半英寸?
挖掘数据
有一种方法我们已经掌握了:循环遍历数据,然后对每个元素的值进行判断是否处在相应的范围。在前面的小节中,我们已经解释了为什么这种方式是低效的原因,无论从写代码花的时间来看还是从计算结果需要的时间来看。在使用Numpy计算:通用函数小节中,我们学习了NumPy的ufuncs可以用来替代循环进行逐个元素的算术计算;同样的,我们也可以使用其他的ufuncs来对每个元素进行比较运算,通过这种方法我们就可以很简单的回答上面问题。我们暂且放下例子的数据,先介绍一些NumPy中用来进行遮盖的通用工具,适合这种任务的处理。
UFuncs的比较运算符
在使用Numpy计算:通用函数小节中,我们介绍了ufuncs,而且主要集中介绍了算术运算符。我们知道可以使用+
、-
、*
、/
和其他的运算可以对数组进行逐个元素的运算操作。NumPy同样也实现了比较运算符如<
(小于)和>
(大于)的ufuncs。这些比较运算符的结算结果一定是一个布尔类型的数组。全部6种标准的比较运算都是支持的:
x = np.array([1, 2, 3, 4, 5])
x < 3 # less than
array([ True, True, False, False, False])
x > 3 # greater than
array([False, False, False, True, True])
x <= 3 # less than or equal
array([ True, True, True, False, False])
x >= 3 # greater than or equal
array([False, False, True, True, True])
x != 3 # not equal
array([ True, True, False, True, True])
x == 3 # equal
array([False, False, True, False, False])
也可以对两个数组的每个元素进行比较,还支持运算的组合操作:
(2 * x) == (x ** 2)
array([False, True, False, False, False])
就像算术运算符一样,比较运算符实际上也是NumPy的ufuncs的简写方式;例如,当你写x < 3
的时候,实际上调用的是NumPy的np.less(x, 3)
。小标列出了比较运算符及其对应的ufuncs:
运算符 | 相应的ufunc | 运算符 | 相应的ufunc | |
---|---|---|---|---|
== |
np.equal |
!= |
np.not_equal |
|
< |
np.less |
<= |
np.less_equal |
|
> |
np.greater |
>= |
np.greater_equal |
如同算术运算ufuncs,比较运算也能应用在任何长度任何形状的数组上。下面是一个二维数组例子:
rng = np.random.RandomState(0)
x = rng.randint(10, size=(3, 4))
x
array([[5, 0, 3, 3], [7, 9, 3, 5], [2, 4, 7, 6]])
x < 6
array([[ True, True, True, True], [False, False, True, True], [ True, True, False, False]])
在任何的情况下,结果都是一个布尔类型数组,NumPy还提供了数量众多的函数能够直接对这些布尔数组进行操作。
print(x)
[[5 0 3 3] [7 9 3 5] [2 4 7 6]]
# 有多少个元素小于6?
np.count_nonzero(x < 6)
8
我们可以看到数组当中有8个元素的值小于6.另一种可选的方法是使用np.sum
;因为在Python中,False
实际上代表0,而True
实际上代表1:
np.sum(x < 6)
8
使用sum()
函数的好处是它的使用就像NumPy的聚合函数一样,可以沿着不同的维度进行计算(如行或列):
# 在每一行中有多少个元素小于6?
np.sum(x < 6, axis=1)
array([4, 2, 2])
上例计算了矩阵中每一行中小于6的元素的个数。
如果我们关心的问题是,是否有任何的元素值或全部的元素值为True,我们可以使用np.any
或np.all
:
# 有没有任何一个元素大于8?
np.any(x > 8)
True
# 有没有任何元素小于0
np.any(x < 0)
False
# 所有的元素都小于10?
np.all(x < 10)
True
# 所有的元素都等于6?
np.all(x == 6)
False
np.all
和np.any
也可以沿着特定的轴进行运算,例如:
# 是否每一行的所有值都小于8?
np.all(x < 8, axis=1)
array([ True, False, True])
上例结果表明,第一行和第三行所有的元素值都小于8,而第二行却不满足。
最后提醒一下:就像在聚合:Min, Max, 以及其他中提示过的一样,Python也有內建的sum()
、any()
和all()
函数。它们和NumPy对应的函数有着不同的语法,特别是应用在多维数组进行计算时,会得到错误和无法预料的结果。你需要保证使用NumPy提供的函数来进行相应的运算。
np.sum((inches > 0.5) & (inches < 1))
29
从结果我们得出结论,雨量介于0.5和1.0英寸之间的天数是29天。
注意上面例子中两个比较运算的括号是必不可少的,因为运算符顺序规定,位运算优于比较运算,因此,如果省略括号,我们会得到下面语句一样的结果,显然是错误的:
inches > (0.5 & inches) < 1
下面的例子使用了一种等同的语法来得到相同的结果,这种写法基于逻辑算术的基本知识:A 且 B 和 非(非A 或 非B)是相等的:
np.sum(~( (inches <= 0.5) | (inches >= 1) ))
29
结合比较运算和布尔运算就可以获得在数组上进行绝大部分逻辑运算的能力。
下表列出了布尔运算符及其对应ufuncs:
运算符 | 相应的ufunc | 运算符 | 相应的ufunc | |
---|---|---|---|---|
& |
np.bitwise_and |
| | np.bitwise_or |
|
^ |
np.bitwise_xor |
~ |
np.bitwise_not |
使用这些工具,我们可以回头来解答前面例子中关于雨量的四个问题。下面的代码就是我们结合遮盖和聚合之后得到的问题的答案:
print("无雨的天数 :", np.sum(inches == 0))
print("有雨的天数 :", np.sum(inches != 0))
print("雨量大于0.5英寸的天数 :", np.sum(inches > 0.5))
print("雨量小于0.2英寸的有雨天数:", np.sum((inches > 0) & (inches < 0.2)))
无雨的天数 : 215 有雨的天数 : 150 雨量大于0.5英寸的天数 : 37 雨量小于0.2英寸的有雨天数: 75
x
array([[5, 0, 3, 3], [7, 9, 3, 5], [2, 4, 7, 6]])
使用下面的比较运算很容易得到一个布尔数组,指代每个元素是否小于5:
x < 5
array([[False, True, True, True], [False, False, True, False], [ True, True, False, False]])
下面我们来从数组中选择符合条件的值出来,我们可以将上面得到的布尔数组作为索引带入数组中,成为遮盖操作:
x[x < 5]
array([0, 3, 3, 3, 2, 4])
返回的是一个一维数组,里面的每个元素都满足条件:那就是结果数组中出现的元素对应的是遮盖布尔数组相应位置上为True
真值。
然后就可以灵活应用遮盖方法来获得我们需要的值了。例如,下面例子计算了很多西雅图雨量数据集相关的统计值:
# 下雨天的遮盖数组
rainy = (inches > 0)
# 夏天的遮盖数组(6月21日是一年的第172天)
days = np.arange(365)
summer = (days > 172) & (days < 262)
print("2014年下雨天雨量中位数(英寸):", np.median(inches[rainy]))
print("2014年夏天雨量中位数(英寸):", np.median(inches[summer]))
print("2014年夏天雨量最大值(英寸):",np.max(inches[summer]))
print("除夏季外其他下雨天雨量中位数(英寸):", np.median(inches[rainy & ~summer]))
2014年下雨天雨量中位数(英寸): 0.19488188976377951 2014年夏天雨量中位数(英寸): 0.0 2014年夏天雨量最大值(英寸): 0.8503937007874016 除夏季外其他下雨天雨量中位数(英寸): 0.20078740157480315
结合布尔操作、遮盖操作和聚合操作,我们可以很快在数据集中得到这类问题的答案。
bool(42), bool(0)
(True, False)
bool(42 and 0)
False
bool(42 or 0)
True
当你在整数上使用&
和|
运算时,这两个操作会运算整数中的每个二进制位,在每个二进制位上执行二进制与或二进制或操作:
bin(42)
'0b101010'
bin(59)
'0b111011'
bin(42 & 59)
'0b101010'
bin(42 | 59)
'0b111011'
对比一下上面例子中的结果是如何从操作数上进行二进制运算获得的。
当数组是一个NumPy的布尔数组时,你可以将这个布尔数组想象成它是由一系列二进制位组成的,因为1 = True
和0 = False
,所以使用&
和|
运算得到的结果类似上面的例子:
A = np.array([1, 0, 1, 0, 1, 0], dtype=bool)
B = np.array([1, 1, 1, 0, 1, 1], dtype=bool)
A | B
array([ True, True, True, False, True, True])
在数组间使用or
操作时,等同于要求Python把数组当成一个整体来求出最终的真值或假值,这样的值是不存在的,因此会导致一个错误:
A or B
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In [39], line 1 ----> 1 A or B ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
类似的,当对于给定的数组进行布尔表达式运算时,你应该使用|
或&
,而不是or
或and
:
x = np.arange(10)
(x > 4) & (x < 8)
array([False, False, False, False, False, True, True, True, False, False])
同样如果试图把数组当成一个整体计算最终真值或假值也是不被允许的,结果还是我们前面看到的那个ValueError
:
(x > 4) and (x < 8)
--------------------------------------------------------------------------- ValueError Traceback (most recent call last) Cell In [41], line 1 ----> 1 (x > 4) and (x < 8) ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
因此,你只需要记住: and
和 or
对整个对象进行单个布尔操作,而&
和|
会对一个对象进行多个布尔操作(比如其中每个二进制位)。对于NumPy布尔数组来说,需要的总是后两者。