02
本章目录:
- 1.1.在数组中求总和
- 1.2.最小值和最大值
- 1.2.1.多维聚合
- 1.2.2.其他聚合函数
- 1.3.例子:美国总统的平均身高?
- 2.1.广播简介
- 2.2.广播的规则
- 2.2.1.广播规则例子 1
- 2.2.2.广播规则例子 2
- 2.2.3.广播规则例子 3
- 2.3.广播规则实践
- 2.3.1.中心化数组
- 2.3.2.绘制二维函数的图形
- 3.1.例子:计算下雨的天数
- 3.1.1 挖掘数据
- 3.2.UFuncs 的比较运算符
- 3.3.操作布尔数组
- 3.3.1.计算元素个数
- 3.3.2.布尔运算符
- 3.4.使用布尔数组作为遮盖
- 3.5.附加内容:对比使用 and/or 关键字和&/|运算符
1.聚合:Min,Max 和其他
通常来说,当我们面对大量数据时,第一步就是计算数据集的概要统计结果。也许最重要的概要统计数据就是平均值和标准差,它们能归纳出数据集典型的数值,但是其他的聚合函数也很用(如求和、乘积、中位值、最小值和最大值、分位数等)。
NumPy 内建有非常快速的函数用于计算数组的统计值;本节中我们会讨论其中常用的部分。
1.1.在数组中求总和
首先,我们用一个简单例子来计算数组所有元素值的总和。使用 Python 內建的sum
函数:
import numpy as np
代码语言:javascript复制L = np.random.random(100)
sum(L)
代码语言:javascript复制47.64949279166021
NumPy 的sum
函数的语法也差不多,当然,结果也是一样的。
np.sum(L)
代码语言:javascript复制47.649492791660215
然后,因为 NumPy 的函数是编译执行的,因此它的性能会远远超越 Python 的內建函数:
代码语言:javascript复制big_array = np.random.rand(1000000)
%timeit sum(big_array)
%timeit np.sum(big_array)
代码语言:javascript复制220 ms ± 8.69 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
1.47 ms ± 30.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
要注意的是:sum
内建函数和np.sum
并不完全相同,这有时会导致混乱。特别的,两个函数的可选参数有着不同的含义,而且np.sum
函数可以处理多维数组运算,我们将在后续章节看到。
1.2.最小值和最大值
类似的,Python 也有內建min
和max
函数,用来计算数组的最小值和最大值:
min(big_array), max(big_array)
代码语言:javascript复制(1.392071186878674e-06, 0.9999991228230799)
NumPy 对应的函数也有相似的语法,但是执行高效很多:
代码语言:javascript复制np.min(big_array), np.max(big_array)
代码语言:javascript复制(1.392071186878674e-06, 0.9999991228230799)
代码语言:javascript复制%timeit min(big_array)
%timeit np.min(big_array)
代码语言:javascript复制127 ms ± 5.94 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
620 µs ± 46.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
对于min
,max
,sum
和其他 NumPy 聚合函数来说,也可以通过ndarray
对象的相应方法进行调用:
print(big_array.min(), big_array.max(), big_array.sum())
代码语言:javascript复制1.392071186878674e-06 0.9999991228230799 500182.29596081574
任何情况下,当你操作 NumPy 数组时,你都应该使用 NumPy 的聚合函数来代替 Python 的內建函数。
1.2.1.多维聚合
还有一种需求,我们可能需要沿着行或列进行聚合。比方说你有一个二维数组:
代码语言:javascript复制M = np.random.random((3, 4))
print(M)
代码语言:javascript复制[[0.68085806 0.93105604 0.7892969 0.11360169]
[0.78627685 0.62677582 0.94028335 0.26876957]
[0.11487821 0.53604559 0.08473022 0.99818919]]
默认情况下,NumPy 聚合函数都会返回整个数组的聚合结果标量:
代码语言:javascript复制M.sum()
代码语言:javascript复制6.8707614958928955
聚合函数可以接收一个额外的参数指定一个轴让函数沿着这个方向进行聚合运算。例如,我们可以沿着行的方向计算每列的最小值,通过指定axis=0
参数即可:
M.min(axis=0)
代码语言:javascript复制array([0.11487821, 0.53604559, 0.08473022, 0.11360169])
这个函数返回四个值,对应着四列。
类似的,我们也可以计算每一行的最大值:
代码语言:javascript复制M.max(axis=1)
代码语言:javascript复制array([0.93105604, 0.94028335, 0.99818919])
上述指定 axis 参数的方式可能会让具有其他编程语言基础的用户感到不适应。这里的axis
参数指定的是让数组沿着这个方向进行压缩,而不是指定返回值的方向。因此指定axis=0
意味着第一个维度将被压缩:对于一个二维数组来说,就是数组将沿着列的方向进行聚合运算操作。
1.2.2.其他聚合函数
NumPy 提供了许多其他聚合函数,但是我们不会在这里详细讨论它们。需要说明的是,很多聚合函数都有一个NaN
安全的版本,可以忽略空缺的数据并计算得到正确的结果。NaN
即为 IEEE 标准中浮点数非数值的定义。部分NaN
安全的函数版本是在 NumPy 1.8 之后加入的,因此在老版本的 NumPy 中可能无法使用。
下表列出了 NumPy 中有用的聚合函数:
函数名称 | NaN 安全版本 | 说明 |
---|---|---|
np.sum | np.nansum | 计算总和 |
np.prod | np.nanprod | 计算乘积 |
np.mean | np.nanmean | 计算平均值 |
np.std | np.nanstd | 计算标准差 |
np.var | np.nanvar | 计算方差 |
np.min | np.nanmin | 计算最小值 |
np.max | np.nanmax | 计算最大值 |
np.argmin | np.nanargmin | 寻找最小值的序号 |
np.argmax | np.nanargmax | 寻找最大值的序号 |
np.median | np.nanmedian | 计算中位值 |
np.percentile | np.nanpercentile | 计算百分比分布的对应值 |
np.any | N/A | 是否含有 True 值 |
np.all | N/A | 是否全为 True 值 |
我们在本书后续内容中会经常看到这些聚合函数。
1.3.例子:美国总统的平均身高?
在 NumPy 中使用聚合统计来对一个数据集进行概要说明是非常有用的。下面我们使用美国总统的身高作为一个简单的例子来说明。这些数据存储在文件president_heights.csv里,文件格式就是简单的逗号分隔的文本文件。
我们会使用 Pandas 包来读取文件和提取数据(注意身高单位是厘米)
代码语言:javascript复制import pandas as pd
data = pd.read_csv(r'F:PythonCoolPython数据科学手册datapresident_heights.csv')
heights = np.array(data['height(cm)'])
print(heights)
代码语言:javascript复制[189 170 189 163 183 171 185 168 173 183 173 173 175 178 183 193 178 173
174 183 183 168 170 178 182 180 183 178 182 188 175 179 183 193 182 183
177 185 188 188 182 185]
获得了 NumPy 数组之后,我们就能计算各种的基本统计数据了:
代码语言:javascript复制print("Mean height: ", heights.mean()) # 身高平均值
print("Standard deviation:", heights.std()) # 标准差
print("Minimum height: ", heights.min()) # 最小值
print("Maximum height: ", heights.max()) # 最大值
代码语言:javascript复制Mean height: 179.73809523809524
Standard deviation: 6.931843442745892
Minimum height: 163
Maximum height: 193
上述结果中,每个聚合函数都将整个数组计算后得到一个标量值,可以让我们初步了解数据的基本分布信息。下面来计算分位值:
代码语言:javascript复制print("25th percentile: ", np.percentile(heights, 25)) # 25% 分位值
print("Median: ", np.median(heights)) # 50% 分位值 - 中位值
print("75th percentile: ", np.percentile(heights, 75)) # 75% 分位值
代码语言:javascript复制25th percentile: 174.25
Median: 182.0
75th percentile: 183.0
我们看到美国总统身高的中位值是 182 厘米,也就是 6 英尺。
当然,有时对数据进行图表展示会更加直观,我们可以通过 Matplotlib 工具进行。例如,下述代码产生相应的图表:
代码语言:javascript复制%matplotlib inline
import matplotlib.pyplot as plt
import seaborn; seaborn.set() # 设置图表的风格为seaborn
代码语言:javascript复制plt.hist(heights)
plt.title('Height Distribution of US Presidents')
plt.xlabel('height (cm)')
plt.ylabel('number');
2.在数组上计算:广播
我们在前面的章节中学习了 NumPy 的通用函数,它们用来对数组进行向量化操作,从而抛弃了性能低下的 Python 循环。还有一种对 NumPy 数组进行向量化操作的方式我们称为广播。广播简单来说就是一整套用于在不同尺寸或形状的数组之间进行二元 ufuncs 运算(如加法、减法、乘法等)的规则。
2.1.广播简介
回忆一下对于相同尺寸的数组来说,二元运算是按每个元素进行运算的:
代码语言:javascript复制import numpy as np
代码语言:javascript复制a = np.array([0, 1, 2])
b = np.array([5, 5, 5])
a b
代码语言:javascript复制array([5, 6, 7])
广播机制允许这样的二元运算能够在不同尺寸和形状的数组之间进行,例如,我们可以用数组和一个标量相加(标量可以认为是一个零维数组):
代码语言:javascript复制a 5
代码语言:javascript复制array([5, 6, 7])
我们可以认为上面的运算首先将标量扩展成了一个一维的数组[5, 5, 5]
,然后在和a
进行了加法运算。NumPy 的广播方式并不是真的需要将元素复制然后扩展,但是这对于理解广播的运行方式很有帮助。
我们可以很简单的将上面的情形推广到更高纬度的数组上。下面我们使用广播将一个一维数组和一个二维数组进行加法运算:
代码语言:javascript复制M = np.ones((3, 3))
M
代码语言:javascript复制array([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]])
代码语言:javascript复制M a
代码语言:javascript复制array([[1., 2., 3.],
[1., 2., 3.],
[1., 2., 3.]])
上例中一维数组a
在第二个维度上进行了扩展或者广播,这样才能符合M
的形状。
上面两个例子相对来说非常容易理解,但是当参与运算的两个数组都需要广播时,情况就相对复杂一些了。看下面的例子:
代码语言:javascript复制a = np.arange(3)
b = np.arange(3)[:, np.newaxis]
print(a)
print(b)
代码语言:javascript复制[0 1 2]
[[0]
[1]
[2]]
代码语言:javascript复制a b
代码语言:javascript复制array([[0, 1, 2],
[1, 2, 3],
[2, 3, 4]])
前面例子中我们只对其中一个数组进行了扩展或者广播,上例中我们需要对a
和b
两个数组都进行广播才能满足双方是相同的形状,最后的结果是一个二维的数组。上面例子可以用下面的图来进行说明,其中部分使用了经过授权的astroML[1]网站文档中的代码)。
Broadcasting Visual
浅色格子代表的是广播后的值:再次说明,这些广播的值不会真正占用内存,只是为了辅助我们理解广播的机制。
2.2.广播的规则
在 NumPy 中应用广播不是随意的,而是需要遵从严格的一套规则:
- 规则 1:如果两个数组有着不同的维度,维度较小的那个数组会沿着最前(或最左)的维度进行扩增,扩增的维度尺寸为 1,这时两个数组具有相同的维度。
- 规则 2:如果两个数组形状在任何某个维度上存在不相同,那么两个数组中形状为 1 的维度都会广播到另一个数组对应唯独的尺寸,最终双方都具有相同的形状。
- 规则 3:如果两个数组在同一个维度上具有不为 1 的不同长度,那么将产生一个错误。
为了说明白这些规则,我们需要参考下面的一些例子:
2.2.1.广播规则例子 1
我们先看一下一个二维数组和一个一维数组相加:
代码语言:javascript复制M = np.ones((2, 3))
a = np.arange(3)
我们先看一下两个数组的形状:
M.shape = (2, 3)
a.shape = (3,)
依据规则 1,数组a
的维度较少,因此首先对其进行维度扩增,我们在其最前面(最左边)增加一个维度,长度为 1。此时两个数组的形状变为:
M.shape -> (2, 3)
a.shape -> (1, 3)
依据规则 2,我们可以看到双方在第一维度上不相同,因此我们将第一维度具有长度 1 的a
的第一维度扩展为 2。此时双方的形状变为:
M.shape -> (2, 3)
a.shape -> (2, 3)
经过变换之后,双方形状一致,可以进行加法运算了,我们可以预知最终结果的形状为(2, 3)
:
M a
代码语言:javascript复制array([[1., 2., 3.],
[1., 2., 3.]])
2.2.2.广播规则例子 2
让我们看一个两个数组都需要广播的情况:
代码语言:javascript复制a = np.arange(3).reshape((3, 1))
b = np.arange(3)
开始时双方的形状为:
a.shape = (3, 1)
b.shape = (3,)
由规则 1 我们需要将数组b
扩增第一维度,长度为 1:
a.shape -> (3, 1)
b.shape -> (1, 3)
由规则 2 我们需要将数组a
的第二维度扩展为 3,还需要将数组b
的第一维度扩展为 3,得到:
a.shape -> (3, 3)
b.shape -> (3, 3)
双方形状相同,可以进行运算:
代码语言:javascript复制a b
代码语言:javascript复制array([[0, 1, 2],
[1, 2, 3],
[2, 3, 4]])
2.2.3.广播规则例子 3
现在我们来看一个不能适用于广播的例子:
代码语言:javascript复制M = np.ones((3, 2))
a = np.arange(3)
这个例子和例子 1 有一点点区别,那就是本例中的M
是例子 1 中M
的转置矩阵。它们的形状是:
M.shape = (3, 2)
a.shape = (3,)
由规则 1 我们需要在数组a
上扩增第一维度,长度为 1:
M.shape -> (3, 2)
a.shape -> (1, 3)
由规则 2 我们需要将数组a
的第一维度扩展为 3 才能与数组M
保持一致,除此之外双方都没有长度为 1 的维度了:
M.shape -> (3, 2)
a.shape -> (3, 3)
观察得到的形状,你可以发现这个结果满足规则 3,双方的各维度长度不完全一致且不为 1,因此无法完成广播,最终会产生错误:
代码语言:javascript复制M a
代码语言:javascript复制---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-38-8cac1d547906> in <module>
----> 1 M a
ValueError: operands could not be broadcast together with shapes (3,2) (3,)
这里你可能会发现一个问题:如果广播的时候不一定按照最前面(最左边)维度的原则进行扩增维度的话,那不是很多的数组都可以进行广播计算吗?这样处理不是更灵活吗?例如上例中如果我们在数组a
的第二维度上扩增的话,那广播就能正确进行了。很可惜,广播并不会支持这种处理方式,虽然这种方法在某些情况下会更加灵活,但是在部分情况下会带来不确定性。如果你确实希望进行右维度扩增的话,你必须明确指定。利用我们在NumPy 数组基础中介绍的np.newaxis
属性可以进行这个操作:
a[:, np.newaxis].shape
代码语言:javascript复制(3, 1)
代码语言:javascript复制M a[:, np.newaxis]
代码语言:javascript复制array([[1., 1.],
[2., 2.],
[3., 3.]])
还要说明的是,上面的例子中我们都是使用加法进行说明,实际上广播可以应用到任何的二元 ufunc 上。例如下面我们采用logaddexp(a, b)
函数求值,这个函数计算的是
的值,使用这个函数能比采用原始的 exp 和 log 函数进行计算得到更高的精度:
代码语言:javascript复制np.logaddexp(M, a[:, np.newaxis])
代码语言:javascript复制array([[1.31326169, 1.31326169],
[1.69314718, 1.69314718],
[2.31326169, 2.31326169]])
更多关于通用函数的介绍,请复习使用 Numpy 计算:通用函数。
2.3.广播规则实践
广播操作在本书后面很多例子中都会见到。因此这里我们看一些简单的例子,更好的说明它。
2.3.1.中心化数组
在前一节中,我们看到了 ufuncs 提供了我们可以避免使用 Python 循环的低效方式,而广播则大大扩展了这种能力。一个常见的例子就是我们需要将数据集进行中心化。例如我们我们进行了 10 次采样观测,每次都会得到 3 个数据值。按照惯例,我们可以将这些数据存成一个
的数组:
代码语言:javascript复制X = np.random.random((10, 3))
我们使用mean
函数沿着第一维度求出每个特征的平均值:
Xmean = X.mean(0)
Xmean
代码语言:javascript复制array([0.5057956 , 0.61465476, 0.53196812])
下面我们就可以将数组X
减去它的各维度平均值就可以将其中心化(这里就是一个广播操作):
X_centered = X - Xmean
我们来检查一下结果的正确性,我们可以通过查看中心化后的数组在各特征上的平均值是够接近于 0 来进行判断:
代码语言:javascript复制X_centered.mean(0)
代码语言:javascript复制array([ 2.22044605e-17, -1.22124533e-16, 7.77156117e-17])
考虑到机器精度情况,平均值已经等于 0 了。
2.3.2.绘制二维函数的图形
广播还有一个很有用的场景,就是当你需要绘制一个二维函数的图像时。如果我们希望定义一个函数
,广播可以被用来计算二维平面上每个网格的数值:
代码语言:javascript复制# x和y都是0~5范围平均分的50个点
x = np.linspace(0, 5, 50)
y = np.linspace(0, 5, 50)[:, np.newaxis]
z = np.sin(x) ** 10 np.cos(10 y * x) * np.cos(x)
算出 z 后,我们使用 Matplotlib 来画出这个二维数组):
代码语言:javascript复制%matplotlib inline
import matplotlib.pyplot as plt
代码语言:javascript复制plt.imshow(z, origin='lower', extent=[0, 5, 0, 5],
cmap='viridis')
plt.colorbar();
上面的图形以一种极其吸引人的方式为我们展现了二维函数的分布情况。
3.比较,遮盖和布尔逻辑
本小节将介绍使用布尔遮盖(掩码)来测试和操作 NumPy 数组的知识。当我们想通过一些标准对数组中的元素值进行提取、修改、计数或者其他一些操作的时候,我们需要使用遮盖:例如,你需要计算所有大于某个特定值的元素个数,或者删除那些超出阈值的离群值。在 NumPy 当中,布尔遮盖基本上是实现这类任务的最有效方式。
3.1.例子:计算下雨的天数
设想你有一系列数据代表着某个城市一年中每天的降水量。例如,下面我们将使用 Pandas 读取 2014 年西雅图的每天降雨统计数据:
代码语言:javascript复制import numpy as np
import pandas as pd
# 使用Pandas读取降水量以英寸为单位的数据
rainfall = pd.read_csv(r'F:PythonCoolPython数据科学手册notebooksdataSeattle2014.csv')['PRCP'].values
inches = rainfall / 254.0 # 0.1毫米转换成英寸
inches.shape
代码语言:javascript复制(365,)
这个数组包含着 365 个元素值,这些值代表着西雅图市 2014 年从 1 月 1 日到 12 月 31 日的降雨(单位英寸)。
我们使用图表可视化展示一下,用简单的直方图来画出降雨天数的分布情况。这里需要使用到 Matplotlib:
代码语言:javascript复制%matplotlib inline
import matplotlib.pyplot as plt
import seaborn; seaborn.set() # 设置图表的风格,seaborn
代码语言:javascript复制plt.hist(inches, 40); # 将降水量区间40等分作为横轴,将落在区间的元素个数作为纵轴
上面的直方图给我们提供了一个对这个数据集的通用观察结论:虽然名声在外,但事实上西雅图在 2014 年中绝大部分日子的降雨量都接近于 0。但是这张图并没有帮助我们了解一些我们希望得到的数据:例如,一年之中有多少天在下雨?下雨的日子中降水量的平均值是多少?一年之中有多少天降水量超过半英寸?
3.1.1 挖掘数据
有一种方法我们已经掌握了:循环遍历数据,然后对每个元素的值进行判断是否处在相应的范围。在前面的小节中,我们已经解释了为什么这种方式是低效的原因,无论从写代码花的时间来看还是从计算结果需要的时间来看。在使用 Numpy 计算:通用函数小节中,我们学习了 NumPy 的 ufuncs 可以用来替代循环进行逐个元素的算术计算;同样的,我们也可以使用其他的 ufuncs 来对每个元素进行比较运算,通过这种方法我们就可以很简单的回答上面问题。我们暂且放下例子的数据,先介绍一些 NumPy 中用来进行遮盖的通用工具,适合这种任务的处理。
3.2.UFuncs 的比较运算符
在使用 Numpy 计算:通用函数小节中,我们介绍了 ufuncs,而且主要集中介绍了算术运算符。我们知道可以使用
、-
、*
、/
和其他的运算可以对数组进行逐个元素的运算操作。NumPy 同样也实现了比较运算符如<
(小于)和>
(大于)的 ufuncs。这些比较运算符的结算结果一定是一个布尔类型的数组。全部 6 种标准的比较运算都是支持的:
x = np.array([1, 2, 3, 4, 5])
代码语言:javascript复制x < 3 # less than
代码语言:javascript复制array([ True, True, False, False, False])
代码语言:javascript复制x > 3 # greater than
代码语言:javascript复制array([False, False, False, True, True])
代码语言:javascript复制x <= 3 # less than or equal
代码语言:javascript复制array([ True, True, True, False, False])
代码语言:javascript复制x >= 3 # greater than or equal
代码语言:javascript复制array([False, False, True, True, True])
代码语言:javascript复制x != 3 # not equal
代码语言:javascript复制array([ True, True, False, True, True])
代码语言:javascript复制x == 3 # equal
代码语言:javascript复制array([False, False, True, False, False])
也可以对两个数组的每个元素进行比较,还支持运算的组合操作:
代码语言:javascript复制(2 * x) == (x ** 2)
代码语言:javascript复制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,比较运算也能应用在任何长度任何形状的数组上。下面是一个二维数组例子:
代码语言:javascript复制rng = np.random.RandomState(0)
x = rng.randint(10, size=(3, 4))
x
代码语言:javascript复制array([[5, 0, 3, 3],
[7, 9, 3, 5],
[2, 4, 7, 6]])
代码语言:javascript复制x < 6
代码语言:javascript复制array([[ True, True, True, True],
[False, False, True, True],
[ True, True, False, False]])
在任何的情况下,结果都是一个布尔类型数组,NumPy 还提供了数量众多的函数能够直接对这些布尔数组进行操作。
3.3.操作布尔数组
对于一个布尔数组,你可以进行许多有用的操作。我们继续使用上面我们创建的二维数组x
来说明。
print(x)
代码语言:javascript复制[[5 0 3 3]
[7 9 3 5]
[2 4 7 6]]
3.3.1.计算元素个数
要计算一个布尔数组的真值True
元素的个数,np.count_nonzero
可以做到:
# 有多少个元素小于6?
np.count_nonzero(x < 6)
代码语言:javascript复制8
我们可以看到数组当中有 8 个元素的值小于 6.另一种可选的方法是使用np.sum
;因为在 Python 中,False
实际上代表 0,而True
实际上代表 1:
np.sum(x < 6)
代码语言:javascript复制8
使用sum()
函数的好处是它的使用就像 NumPy 的聚合函数一样,可以沿着不同的维度进行计算(如行或列):
# 在每一行中有多少个元素小于6?
np.sum(x < 6, axis=1)
代码语言:javascript复制array([4, 2, 2])
上例计算了矩阵中每一行中小于 6 的元素的个数。
如果我们关心的问题是,是否有任何的元素值或全部的元素值为 True,我们可以使用np.any
或np.all
:
# 有没有任何一个元素大于8?
np.any(x > 8)
代码语言:javascript复制True
代码语言:javascript复制# 有没有任何元素小于0
np.any(x < 0)
代码语言:javascript复制False
代码语言:javascript复制# 所有的元素都小于10?
np.all(x < 10)
代码语言:javascript复制True
代码语言:javascript复制# 所有的元素都等于6?
np.all(x == 6)
代码语言:javascript复制False
np.all
和np.any
也可以沿着特定的轴进行运算,例如:
# 是否每一行的所有值都小于8?
np.all(x < 8, axis=1)
代码语言:javascript复制array([ True, False, True])
上例结果表明,第一行和第三行所有的元素值都小于 8,而第二行却不满足。
最后提醒一下:就像在聚合:Min, Max, 以及其他中提示过的一样,Python 也有內建的sum()
、any()
和all()
函数。它们和 NumPy 对应的函数有着不同的语法,特别是应用在多维数组进行计算时,会得到错误和无法预料的结果。你需要保证使用 NumPy 提供的函数来进行相应的运算。
3.3.2.布尔运算符
我们已经学习到了如何计算雨量小于 4 英寸的天数或者雨量大于 2 英寸的天数。但是如果我们期望的结果是雨量小于 4 英寸并且大于 1 英寸的天数,该怎么做?这可以通过 Python 的位运算符来实现,包括&
、|
、^
和~
。就像普通的算术运算符一样,NumPy 重载了这些符号作为 ufuncs,可以在数组(通常是布尔数组)每个元素值上进行位操作。
例如,我们可以进行下面这个复合运算操作:
代码语言:javascript复制np.sum((inches > 0.5) & (inches < 1))
代码语言:javascript复制29
从结果我们得出结论,雨量介于 0.5 和 1.0 英寸之间的天数是 29 天。
注意上面例子中两个比较运算的括号是必不可少的,因为运算符顺序规定,位运算优于比较运算,因此,如果省略括号,我们会得到下面语句一样的结果,显然是错误的:
代码语言:javascript复制inches > (0.5 & inches) < 1
下面的例子使用了一种等同的语法来得到相同的结果,这种写法基于逻辑算术的基本知识:A 且 B 和 *非(非 A 或 非 B)*是相等的:
代码语言:javascript复制np.sum(~( (inches <= 0.5) | (inches >= 1) ))
代码语言:javascript复制29
结合比较运算和布尔运算就可以获得在数组上进行绝大部分逻辑运算的能力。
下表列出了布尔运算符及其对应 ufuncs:
运算符 | 相应的 ufunc | 运算符 | 相应的 ufunc |
---|---|---|---|
& | np.bitwise_and | | | np.bitwise_or |
^ | np.bitwise_xor | ~ | np.bitwise_not |
使用这些工具,我们可以回头来解答前面例子中关于雨量的四个问题。下面的代码就是我们结合遮盖和聚合之后得到的问题的答案:
代码语言:javascript复制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)))
代码语言:javascript复制无雨的天数 : 215
有雨的天数 : 150
雨量大于0.5英寸的天数 : 37
雨量小于0.2英寸的有雨天数: 75
3.4.使用布尔数组作为遮盖
在刚才的例子中,我们在布尔数组上应用聚合操作,得到结果。一个更加有用的场景是使用布尔数组作为遮盖,用来从数据集中选择目标数据出来。回到前面数组x
的例子,如果我们要选择数组中所有小于 5 的元素,可以这样做:
x
代码语言:javascript复制array([[5, 0, 3, 3],
[7, 9, 3, 5],
[2, 4, 7, 6]])
使用下面的比较运算很容易得到一个布尔数组,指代每个元素是否小于 5:
代码语言:javascript复制x < 5
代码语言:javascript复制array([[False, True, True, True],
[False, False, True, False],
[ True, True, False, False]])
下面我们来从数组中选择符合条件的值出来,我们可以将上面得到的布尔数组作为索引带入数组中,成为遮盖操作:
代码语言:javascript复制x[x < 5]
代码语言:javascript复制array([0, 3, 3, 3, 2, 4])
返回的是一个一维数组,里面的每个元素都满足条件:那就是结果数组中出现的元素对应的是遮盖布尔数组相应位置上为True
真值。
然后就可以灵活应用遮盖方法来获得我们需要的值了。例如,下面例子计算了很多西雅图雨量数据集相关的统计值:
代码语言:javascript复制# 下雨天的遮盖数组
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]))
代码语言:javascript复制2014年下雨天雨量中位数(英寸): 0.19488188976377951
2014年夏天雨量中位数(英寸): 0.0
2014年夏天雨量最大值(英寸): 0.8503937007874016
除夏季外其他下雨天雨量中位数(英寸): 0.20078740157480315
结合布尔操作、遮盖操作和聚合操作,我们可以很快在数据集中得到这类问题的答案。
3.5.附加内容:对比使用 and/or 关键字和&/|运算符
使用关键字and
和or
,与使用运算符&
和|
,两者的区别,常常会困惑很多人。什么情况下你应该用哪种运算呢?
区别在于:and
和or
用在将整个对象当成真值或假值进行运算的场合,而&
和|
会针对每个对象内的二进制位进行运算。
当你使用and
或or
的时候,相当于要求 Python 将对象当成是一个布尔值的整体。在 Python 中,所有的非 0 值都会被演算成 True,因此:
bool(42), bool(0)
代码语言:javascript复制(True, False)
代码语言:javascript复制bool(42 and 0)
代码语言:javascript复制False
代码语言:javascript复制bool(42 or 0)
代码语言:javascript复制True
当你在整数上使用&
和|
运算时,这两个操作会运算整数中的每个二进制位,在每个二进制位上执行二进制与或二进制或操作:
bin(42)
代码语言:javascript复制'0b101010'
代码语言:javascript复制bin(59)
代码语言:javascript复制'0b111011'
代码语言:javascript复制bin(42 & 59)
代码语言:javascript复制'0b101010'
代码语言:javascript复制bin(42 | 59)
代码语言:javascript复制'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
代码语言:javascript复制array([ True, True, True, False, True, True])
在数组间使用or
操作时,等同于要求 Python 把数组当成一个整体来求出最终的真值或假值,这样的值是不存在的,因此会导致一个错误:
A or B
代码语言:javascript复制---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-93-ea2c97d9d9ee> 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()
类似的,当对于给定的数组进行布尔表达式运算时,你应该使用|
或&
,而不是or
或and
:
x = np.arange(10)
(x > 4) & (x < 8)
代码语言:javascript复制array([False, False, False, False, False, True, True, True, False,
False])
同样如果试图把数组当成一个整体计算最终真值或假值也是不被允许的,结果还是我们前面看到的那个ValueError
:
(x > 4) and (x < 8)
代码语言:javascript复制---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-95-eecf1fdd5fb4> 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()
因此,你只需要记住:and
和or
对整个对象进行单个布尔操作,而&
和|
会对一个对象进行多个布尔操作(比如其中每个二进制位)。对于 NumPy 布尔数组来说,需要的总是后两者。
参考资料
[1]
astroML: http://astroml.org