数据科学 IPython 笔记本 9.8 比较,掩码和布尔逻辑

281 阅读10分钟
原文链接: github.com

9.8 比较,掩码和布尔逻辑

本节是《Python 数据科学手册》(Python Data Science Handbook)的摘录。

译者:飞龙

协议:CC BY-NC-SA 4.0

本节介绍如何使用布尔掩码,来检查和操作 NumPy 数组中的值。当你想要根据某些标准,提取,修改,计算或以其他方式操纵数组中的值时,掩码会有所帮助:例如,你可能希望计算大于某个值的所有值,或者可能删除高于某些阈值的所有异常值。

在 NumPy 中,布尔掩码通常是完成这些类型任务的最有效方法。

示例:统计雨天

想象一下,你有一系列数据表示某一城市一年中每天的降水量。例如,在这里我们将使用 Pandas 加载 2014 年西雅图市的每日降雨量统计数据(在第三章中有更详细的介绍):

import numpy as np
import pandas as pd

# 使用 pandas 将降雨量英寸提取为 NumPy 数组
rainfall = pd.read_csv('data/Seattle2014.csv')['PRCP'].values
inches = rainfall / 254  # 1/10mm -> 英寸
inches.shape

# (365,)

该数组包含 365 个值,提供了 2014 年 1 月 1 日至 12 月 31 日的每日降雨量,单位为英寸。

作为第一个简单的可视化,让我们看一下使用 Matplotlib 生成的雨天的直方图(我们将在第四章中更全面地探索这个工具):

%matplotlib inline
import matplotlib.pyplot as plt
import seaborn; seaborn.set()  # 设置绘图风格

plt.hist(inches, 40);

png

这个直方图让我们对数据的概况有了一个大概的了解:尽管它的声誉很高,但 2014 年西雅图的绝大多数日子的测得的降雨量几乎为零。但这并没有很好地传达我们希望看到的一些信息:例如,一年中有多少雨天?那些下雨天的平均降雨量是多少? 有多少天有超过半英寸的降雨?

挖掘数据

一种方法是手动回答这些问题:遍历数据,每当我们看到某个所需范围内的值时,递增计数器。由于本章讨论的原因,从编写代码的时间和计算结果的时间的角度来看,这种方法效率非常低。

我们在“NumPy 上的数组计算:通用函数”中看到,NumPy 的ufuncs可用于代替循环,对数组进行快速的逐元素算术运算;以同样的方式,我们可以使用其他ufunc对数组进行逐元素比较,然后我们可以操纵结果来回答我们的问题。

我们现在暂时搁置数据,并讨论 NumPy 中的一些常用工具,使用掩码快速回答这类的问题。

作为ufunc的比较运算

在“NumPy 上的数组计算:通用函数”中,我们介绍了ufunc,专注于算术运算符。 我们看到,在数组上使用+-*/和其他,产生了逐元素操作。

NumPy 还将比较运算符,例如<(小于)和>(大于),实现为逐元素的ufunc。这些比较运算符的结果始终是布尔数据类型的数组。所有六种标准比较操作都可用:

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

x < 3  # 小于

# array([ True,  True, False, False, False], dtype=bool)

x > 3  # 大于

# array([False, False, False,  True,  True], dtype=bool)

x <= 3  # 小于等于

# array([ True,  True,  True, False, False], dtype=bool)

x >= 3  # 大于等于

# array([False, False,  True,  True,  True], dtype=bool)

x != 3  # 不等于

# array([ True,  True, False,  True,  True], dtype=bool)

x == 3  # 等于

# array([False, False,  True, False, False], dtype=bool)

也可以对两个数组进行逐元素比较,并包含复合表达式:

(2 * x) == (x ** 2)

# array([False,  True, False, False, False], dtype=bool)

与算术运算符的情况一样,比较运算符在 NumPy 中实现为ufunc;例如,当你编写x <3时,NumPy 内部使用np.less(x, 3)

此处显示了比较运算符及其等价ufunc的摘要:

运算符 等价 ufunc 运算符 等价 ufunc
== np.equal != np.not_equal
< np.less <= np.less_equal
> np.greater >= np.greater_equal

就像算术ufunc的情况一样,这些适用于任何大小和形状的数组。这是一个二维的例子:

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]], dtype=bool)
'''

在每种情况下,结果都是一个布尔数组,NumPy 提供了许多简单的模式来处理这些布尔结果。

使用布尔数组

给定一个布尔数组,你可以执行许多有用的操作。我们将使用x,我们之前创建的二维数组。

print(x)

'''
[[5 0 3 3]
 [7 9 3 5]
 [2 4 7 6]]
'''

对元素计数

要计算布尔数组中True元素的数量,np.count_nonzero很有用:

# 多少个值小于 6
np.count_nonzero(x < 6)

# 8

我们看到有八个小于 6 的数组元素。获取此信息的另一种方法是使用np.sum;在这种情况下,False解释为0,而True解释为1

np.sum(x < 6)

# 8

`sum()``的好处就是和其他NumPy聚合函数一样,这个求和也可以沿着行或列来完成:

# 每一行有多少个值小于 6
np.sum(x < 6, axis=1)

# array([4, 2, 2])

这计算了矩阵每行中小于 6 的值的数量。

如果我们有兴趣快速检查,是否任何或所有值都是真的,我们可以使用(你猜对了)np.anynp.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.allnp.any也可用于特定的轴。例如:

# 每一行的所有值都小于 4 吗?
np.all(x < 8, axis=1)

# array([ True, False,  True], dtype=bool)

这里第一行和第三行中的所有元素都小于 8,而第二行则不是这种情况。

最后,一个简单的警告:如“聚合:最小、最大和之间的任何东西”中所述,Python 内置了sum() any()all()函数。 它们的语法与 NumPy 版本不同,特别是在多维数组上使用时会失败或产生意外结果。对于这些情况,请确保使用np.sum()np.any()np.all(()

布尔运算符

我们已经看到了我们如何计算,比如降雨量小于 4 英寸的所有日子,或降雨量大于 2 英寸的所有日子。但是如果我们想了解降雨量小于 4 英寸且大于 1 英寸的所有日子呢?

这是通过 Python 的按位逻辑运算符,&|^~来实现的。与标准算术运算符一样,NumPy 将这些重载为ufunc,这些ufunc在(通常是布尔)数组上逐元素工作。例如,我们可以像这样解决这种复合问题:

np.sum((inches > 0.5) & (inches < 1))

# 29

所以我们看到有 29 天的降雨量在 0.5 到 1.0 英寸之间。请注意,此处的括号很重要 - 由于运算符优先级规则,删除了括号,此表达式将按如下方式计算,这会导致错误:

inches > (0.5 & inches) < 1

使用A AND BNOT (NOT A OR NOT B)的等价性(如果你已经参加了逻辑入门课程,你可能还记得),我们可以用不同的方式计算相同的结果:

np.sum(~( (inches <= 0.5) | (inches >= 1) ))

# 29

在数组上组合比较运算符和布尔运算符。可以实现广泛的高效逻辑运算。下表总结了按位布尔运算符及其等效的ufunc

运算符 等价 ufunc 运算符 等价 ufunc
& np.bitwise_and | np.bitwise_or
^ np.bitwise_xor ~ np.bitwise_not

使用这些工具,我们可以开始回答有关天气数据的问题。以下是将掩码聚合结合使用时,可以计算的一些结果示例:

print("Number days without rain:      ", np.sum(inches == 0))
print("Number days with rain:         ", np.sum(inches != 0))
print("Days with more than 0.5 inches:", np.sum(inches > 0.5))
print("Rainy days with < 0.2 inches  :", np.sum((inches > 0) &
                                                (inches < 0.2)))
                                                
'''
Number days without rain:       215
Number days with rain:          150
Days with more than 0.5 inches: 37
Rainy days with < 0.2 inches  : 75
'''

作为掩码的布尔数组

在上一节中,我们研究了直接在布尔数组上计算的聚合。更强大的模式是将布尔数组用作掩码,来选择数据本身的特定子集。回到之前的x数组,假设我们想要所有值小于 5 的数组:

x

'''
array([[5, 0, 3, 3],
       [7, 9, 3, 5],
       [2, 4, 7, 6]])
'''

我们可以很容易地获得这样的布尔数组,正如我们已经看到的:

x < 5

'''
array([[False,  True,  True,  True],
       [False, False,  True, False],
       [ True,  True, False, False]], dtype=bool)
'''

现在为了从数组中选择这些值,我们可以简单地用这个布尔数组来索引;这被称为掩码操作:

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("Median precip on rainy days in 2014 (inches):   ",
      np.median(inches[rainy]))
print("Median precip on summer days in 2014 (inches):  ",
      np.median(inches[summer]))
print("Maximum precip on summer days in 2014 (inches): ",
      np.max(inches[summer]))
print("Median precip on non-summer rainy days (inches):",
      np.median(inches[rainy & ~summer]))
      
'''
Median precip on rainy days in 2014 (inches):    0.194881889764
Median precip on summer days in 2014 (inches):   0.0
Maximum precip on summer days in 2014 (inches):  0.850393700787
Median precip on non-summer rainy days (inches): 0.200787401575
'''

通过组合布尔运算,掩码操作和聚合,我们可以非常快速地为我们的数据集回答这些问题。

注:使用关键字and/or与运算符&/|

一个常见的混淆点是,关键字andor,与运算符&|之间的区别。你什么时候使用其中一个?

区别在于:andor衡量整个对象的真实性或错误性,而&|指的是每个对象中的位。当你使用andor时,它等同于要求 Python 将对象视为一个布尔实体。在 Python 中,所有非零整数都将计算为True。 从而:

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 = True0 = 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], dtype=bool)

在这些数组上使用andor,将尝试求解整个数组对象的真实性或错误性,这不是一个明确定义的值:

A or B

'''
---------------------------------------------------------------------------

ValueError                                Traceback (most recent call last)

<ipython-input-38-5d8e4f2e21c0> in <module>()
----> 1 A or B


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

类似地,当在给定数组上执行布尔表达式时,你应该使用|&而不是orand

x = np.arange(10)
(x > 4) & (x < 8)

# array([False, False, False, False, False,  True,  True,  True, False, False], dtype=bool)

试图求解整个数组的真实性或错误性,将给出我们之前看到的相同的ValueError

(x > 4) and (x < 8)

'''
---------------------------------------------------------------------------

ValueError                                Traceback (most recent call last)

<ipython-input-40-3d24f1ffd63d> in <module>()
----> 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()
'''

所以记住这一点:andor对整个对象执行单个布尔求值,而&|对对象的内容(单个位或字节)执行多次布尔求值。对于布尔 NumPy 数组,后者几乎总是所需的操作。