Numpy进阶 Part1
1. NumPy通用函数
NumPy 通用函数的重要性——它可以提高数组元素的重复计算的效率,这也是我们写代码追求的目标。
1.1 NumPy的普通通用函数
通用函数有两种存在形式: 一元通用函数对单个输入操作, 二元通用函数对两个输入操作。
数组的运算:NumPy 通用函数的使用方式非常自然,因为它用到了 Python 原生的算术运算符,标准的
加、减、乘、除都可以使用。
In[7]: x = np.arange(4)
print("x =", x)
print("x + 5 =", x + 5)
print("x - 5 =", x - 5)
print("x * 2 =", x * 2)
print("x / 2 =", x / 2)
print("x // 2 =", x // 2) #地板除法运算
x = [0 1 2 3]
x + 5 = [5 6 7 8]
x - 5 = [-5 -4 -3 -2]
x * 2 = [0 2 4 6]
x / 2 = [ 0. 0.5 1. 1.5]
x // 2 = [0 0 1 1]
# 还有逻辑非、 ** 表示的指数运算符和 % 表示的模运算符的一元通用函数:
In[8]: print("-x = ", -x)
print("x ** 2 = ", x ** 2)
print("x % 2 = ", x % 2)
-x = [ 0 -1 -2 -3]
x ** 2 = [0 1 4 9]
x % 2 = [0 1 0 1]
所有的这些运算都有对应的通用函数,例如:np.add 对应加法运算,np.multiply对应乘法运算等等。
绝对值:
In[11]: x = np.array([-2, -1, 0, 1, 2])
abs(x)
Out[11]: array([2, 1, 0, 1, 2])
# 对应的 NumPy 通用函数是 np.absolute,该函数也可以用别名 np.abs 来访问:
In[12]: np.absolute(x)
Out[12]: array([2, 1, 0, 1, 2])
In[13]: np.abs(x)
Out[13]: array([2, 1, 0, 1, 2])
# 这个通用函数也可以处理复数。当处理复数时,绝对值返回的是该复数的幅度:
In[14]: x = np.array([3 - 4j, 4 - 3j, 2 + 0j, 0 + 1j])
np.abs(x)
Out[14]: array([ 5., 5., 2., 1.])
三角函数:
# 首先定义一个角度数组:
In[15]: theta = np.linspace(0, np.pi, 3)
#现在可以对这些值进行一些三角函数计算:
In[16]: print("theta = ", theta)
print("sin(theta) = ", np.sin(theta))
print("cos(theta) = ", np.cos(theta))
print("tan(theta) = ", np.tan(theta))
theta = [ 0. 1.57079633 3.14159265]
sin(theta) = [ 0.00000000e+00 1.00000000e+00 1.22464680e-16]
cos(theta) = [ 1.00000000e+00 6.12323400e-17 -1.00000000e+00]
tan(theta) = [ 0.00000000e+00 1.63312394e+16 -1.22464680e-16]
指数和对数:
In[18]: x = [1, 2, 3]
print("x =", x)
print("e^x =", np.exp(x))
print("2^x =", np.exp2(x))
print("3^x =", np.power(3, x))
x = [1, 2, 3]
e^x = [ 2.71828183 7.3890561 20.08553692]
2^x = [ 2. 4. 8.]
3^x = [ 3 9 27]
# 指数运算的逆运算,即对数运算也是可用的。最基本的 np.log 给出的是以自然数为底数的对数。如果你希望计算以 2 为底数或者以 10 为底数的对数,可以按照如下示例处理:
In[19]: x = [1, 2, 4, 10]
print("x =", x)
print("ln(x) =", np.log(x))
print("log2(x) =", np.log2(x))
print("log10(x) =", np.log10(x))
x = [1, 2, 4, 10]
ln(x) = [ 0. 0.69314718 1.38629436 2.30258509]
log2(x) = [ 0. 1. 2. 3.32192809]
log10(x) = [ 0. 0.30103 0.60205999 1. ]
内层的列表被当作二维数组的行。
1.2 高级的通用函数特性
指定输出:在进行大量运算时,有时候指定一个用于存放运算结果的数组是非常有用的。不同于创建临时数组,你可以用这个特性将计算结果直接写入到你期望的存储位置。所有的通用函数都可以通过 out 参数来指定计算结果的存放位置。
In[24]: x = np.arange(5)
y = np.empty(5)
np.multiply(x, 10, out=y)
print(y)
[ 0. 10. 20. 30. 40.]
#这个特性也可以被用作数组视图,例如可以将计算结果写入指定数组的每隔一个元素的位置:
In[25]: y = np.zeros(10)
np.power(2, x, out=y[::2])
print(y)
[ 1. 0. 2. 0. 4. 0. 8. 0. 16. 0.]
如果上面例子写的是 y[::2] = 2 * x,那么结果将是创建一个临时数组,该数组存放的是2 * x 的结果,并且接下来会将这些值复制到 y 数组中。对于上述例子中比较小的计算量来说,这两种方式的差别并不大。但是对于较大的数组,通过慎重使用 out 参数将能够有效节约内存。
聚合:二元通用函数有些非常有趣的聚合功能,这些聚合可以直接在对象上计算。
In[30]: x = np.arange(1, 6)
np.multiply.outer(x, x)
Out[30]: array([[ 1, 2, 3, 4, 5],
[ 2, 4, 6, 8, 10],
[ 3, 6, 9, 12, 15],
[ 4, 8, 12, 16, 20],
[ 5, 10, 15, 20, 25]])
# 对 add 通用函数调用 reduce 方法会返回数组中所有元素的和:
In[26]: x = np.arange(1, 6)
np.add.reduce(x)
Out[26]: 15
#同样,对 multiply 通用函数调用 reduce 方法会返回数组中所有元素的乘积:
In[27]: np.multiply.reduce(x)
Out[27]: 120
#如果需要存储每次计算的中间结果,可以使用 accumulate:
In[28]: np.add.accumulate(x)
Out[28]: array([ 1, 3, 6, 10, 15])
In[29]: np.multiply.accumulate(x)
Out[29]: array([ 1, 2, 6, 24, 120])
外积:任何通用函数都可以用 outer 方法获得两个不同输入数组所有元素对的函数运算结果。
2. 聚合:最小值、最大值和其他值
2.1 数组值求和
# Python 本身可用内置的 sum 函数来实现:
In[1]: import numpy as np
In[2]: L = np.random.random(100)
sum(L)
Out[2]: 55.61209116604941
# 它的语法和 NumPy 的 sum 函数非常相似,并且在这个简单的例子中的结果也是一样的:
In[3]: np.sum(L)
Out[3]: 55.612091166049424
#但是,因为 NumPy 的 sum 函数在编译码中执行操作,所以 NumPy 的操作计算得更快一些:
In[4]: big_array = np.random.rand(1000000)
%timeit sum(big_array)
%timeit np.sum(big_array)
10 loops, best of 3: 104 ms per loop
1000 loops, best of 3: 442 µs per loop
2.2 最小值和最大值
Python 也有内置的 min 函数和 max 函数,分别被用于获取给定数组的最小值和最大值:
In[5]: min(big_array), max(big_array)
Out[5]: (1.1717128136634614e-06, 0.9999976784968716)
# NumPy 对应的函数也有类似的语法,并且也执行得更快:
In[6]: np.min(big_array), np.max(big_array)
Out[6]: (1.1717128136634614e-06, 0.9999976784968716)
In[7]: %timeit min(big_array)
%timeit np.min(big_array)
10 loops, best of 3: 82.3 ms per loop
1000 loops, best of 3: 497 µs per loop
2.2.1 多维度聚合
一种常用的聚合操作是沿着一行或一列聚合。
In[9]: M = np.random.random((3, 4))
print(M)
[[ 0.8967576 0.03783739 0.75952519 0.06682827]
[ 0.8354065 0.99196818 0.19544769 0.43447084]
[ 0.66859307 0.15038721 0.37911423 0.6687194]]
# 默认情况下,每一个 NumPy 聚合函数将会返回对整个数组的聚合结果:
In[10]: M.sum()
Out[10]: 6.0850555667307118
聚合函数还有一个参数,用于指定沿着哪个轴的方向进行聚合:
# 可以通过指定axis=0 找到每一列的最小值:
In[11]: M.min(axis=0)
Out[11]: array([ 0.66859307, 0.03783739, 0.19544769, 0.06682827])
#这个函数返回四个值,对应四列数字的计算值。
#同样,也可以找到每一行的最大值:
In[12]: M.max(axis=1)
Out[12]: array([ 0.8967576 , 0.99196818, 0.6687194])
其他语言的用户会对轴的指定方式比较困惑。 axis 关键字指定的是数组将会被折叠的维度,而不是将要返回的维度。因此指定 axis=0 意味着第一个轴将要被折叠——对于二维数组,这意味着每一列的值都将被聚合。
2.2.2 其他聚合函数
大多数的聚合都有对 NaN 值的安全处理策略(NaN-safe),即计算时忽略所有的缺失值,这些缺失值即特殊的 IEEE 浮点型 NaN 。
函数名称 | 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 | 验证任何一个元素是否为真 |
np.all | N/A | 验证所有元素是否为真 |
2.3 广播
广播可以简单理解为用于不同大小数组的二进制通用函数(加、减、乘等)的一组规则。
2.3.1 广播的介绍
对于同样大小的数组,二进制操作是对相应元素逐个计算:
In[1]: import numpy as np
In[2]: a = np.array([0, 1, 2])
b = np.array([5, 5, 5])
a + b
Out[2]: array([5, 6, 7])
广播允许这些二进制操作可以用于不同大小的数组。例如,可以简单地将一个标量(可以认为是一个零维的数组)和一个数组相加:
In[3]: a + 5
Out[3]: array([5, 6, 7])
我们可以认为这个操作是将数值 5 扩展或重复至数组 [5, 5, 5],然后执行加法。 NumPy广播功能的好处是,这种对值的重复实际上并没有发生,但是这是一种很好用的理解广播的模型。
我们同样也可以将这个原理扩展到更高维度的数组。
# 观察以下将一个一维数组和一个二维数组相加的结果:
In[4]: M = np.ones((3, 3))
M
Out[4]: array([[ 1., 1., 1.],
[ 1., 1., 1.],
[ 1., 1., 1.]])
In[5]: M + a
Out[5]: array([[ 1., 2., 3.],
[ 1., 2., 3.],
[ 1., 2., 3.]])
这里这个一维数组就被扩展或者广播了。它沿着第二个维度扩展,扩展到匹配 M 数组的形状。
以上的这些例子理解起来都相对容易,更复杂的情况会涉及对两个数组的同时广播,例如:
In[6]: a = np.arange(3)
b = np.arange(3)[:, np.newaxis]
print(a)
print(b)
[0 1 2]
[[0]
[1]
[2]]
In[7]: a + b
Out[7]: array([[0, 1, 2],
[1, 2, 3],
[2, 3, 4]])
正如此前将一个值扩展或广播以匹配另外一个数组的形状,这里将 a 和 b 都进行了扩展来匹配一个公共的形状,最终的结果是一个二维数组。
浅色的盒子表示广播的值。同样需要注意的是,这个额外的内存并没有在实际操作中进行分配,但是这样的想象方式更方便我们从概念上理解。
2.3.2 广播的规则
NumPy 的广播遵循一组严格的规则,设定这组规则是为了决定两个数组间的操作。
- 规则 1: 如果两个数组的维度数不相同,那么小维度数组的形状将会在最左边补 1。
- 规则 2:如果两个数组的形状在任何一个维度上都不匹配,那么数组的形状会沿着维度为 1 的维度扩展以匹配另外一个数组的形状。
- 规则 3:如果两个数组的形状在任何一个维度上都不匹配并且没有任何一个维度等于 1,那么会引发异常。
示例1:
# 将一个二维数组与一个一维数组相加:
In[8]: 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,第一个维度不匹配,因此扩展这个维度以匹配数组:
M.shape -> (2, 3)
a.shape -> (2, 3)
#现在两个数组的形状匹配了,可以看到它们的最终形状都为 (2, 3):
# 进行计算
In[9]: M + a
Out[9]: array([[ 1., 2., 3.],
[ 1., 2., 3.]])
示例2:
# 来看两个数组均需要广播的示例:
In[10]: a = np.arange(3).reshape((3, 1))
b = np.arange(3)
#同样,首先写出两个数组的形状:
a.shape = (3, 1)
b.shape = (3,)
#规则 1 告诉我们,需要用 1 将 b 的形状补全:
a.shape -> (3, 1)
b.shape -> (1, 3)
#规则 2 告诉我们,需要更新这两个数组的维度来相互匹配:
a.shape -> (3, 3)
b.shape -> (3, 3)
#因为结果匹配,所以这两个形状是兼容的,可以看到以下结果:
In[11]: a + b
Out[11]: array([[0, 1, 2],
[1, 2, 3],
[2, 3, 4]])
示例3:
#现在来看一个两个数组不兼容的示例:
In[12]: M = np.ones((3, 2))
a = np.arange(3)
#和第一个示例相比,这里有个微小的不同之处:矩阵 M 是转置的。那么这将如何影响计算呢?两个数组的形状如下:
M.shape = (3, 2)
a.shape = (3,)
# 同样,规则 1 告诉我们, a 数组的形状必须用 1 进行补全:
M.shape -> (3, 2)
a.shape -> (1, 3)
#根据规则 2, a 数组的第一个维度进行扩展以匹配 M 的维度:
M.shape -> (3, 2)
a.shape -> (3, 3)
#现在需要用到规则 3——最终的形状还是不匹配,因此这两个数组是不兼容的。当我们执行运算时会看到以下结果:
In[13]: M + a
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-13-9e16e9f98da6> in <module>()
----> 1 M + a
ValueError: operands could not be broadcast together with shapes (3,2) (3,)
这里可能发生的混淆在于:你可能想通过在 a 数组的右边补 1,而不是左边补 1,让 a 和 M 的维度变得兼容。但是这不被广播的规则所允许。这种灵活性在有些情景中可能会有用,但是它可能会导致结果模糊。如果你希望实现右边补全,可以通过变形数组来实现:
In[14]: a[:, np.newaxis].shape
Out[14]: (3, 1)
In[15]: M + a[:, np.newaxis]
Out[15]: array([[ 1., 1.],
[ 2., 2.],
[ 3., 3.]])
另外也需要注意,这里仅用到了 + 运算符,而这些广播规则对于任意二进制通用函数都是适用的。