16  NumPy

数值计算是科学研究领域使用最多的编程功能。虽然Python自带了一些基本的数学函数以及列表、元组等数据结构,然而并没有对向量运算的天然支持,因而我们需要NumPy这个包来支持Python中的向量运算。

NumPy提供了丰富的数值计算方法,由于篇幅所限,我们无法介绍所有的NumPy特性,更多的参考可以从NumPy(包括SciPy)的网站: https://docs.scipy.org/doc/ 找到更加详细的参考。

16.1 向量、矩阵及其运算

NumPy最核心的是提供了向量和矩阵两个数据结构,可以方便的使用这些数据结构进行向量的加、减、数乘,以及矩阵的加、减、数乘、乘法、求逆、矩阵分解等运算。

在Python中,如果需要使用NumPy,首先要使用pip安装numpy,接着在文件中导入numpy。numpy.array()函数提供了将一个列表转化为一个向量的方法,比如:

import numpy as np

a=np.array([1,2,3]) #创建一个三维向量
print('向量a=',a)
向量a= [1 2 3]

此外,还有一些其他的方法创建向量,比如:

b=np.ones(3) #创建一个元素全为1的向量
print('b=',b)
b0=np.zeros(3) #创建一个元素全为0的向量
print('b0=',b0)
b= [ 1.  1.  1.]
b0= [ 0.  0.  0.]

还有一个更加常用的函数:linespace(n1,n2,N),该函数创建从n1到n2区间平均分为N等份的网格点:

lp = np.linspace(0, 5, 20)   # 0 到 5 等分 20 等分
print("0 到 5 等分 20 等分:\n",lp)
0到5等分20等分:
 [ 0.          0.26315789  0.52631579  0.78947368  1.05263158  1.31578947
  1.57894737  1.84210526  2.10526316  2.36842105  2.63157895  2.89473684
  3.15789474  3.42105263  3.68421053  3.94736842  4.21052632  4.47368421
  4.73684211  5.        ]

NumPy支持很多向量运算,比如:

print("基本数学运算:元素对元素运算")
print('数乘:3*a=',3*a)
print('a-b=',a-b)
print('a+b=',a+b)
print('a*b=',a*b)
print('b/a=',b/a)
print('向量运算')
c=np.dot(a,b) #a.*b
print('点乘(内积)a.*b=',c)
d=np.outer(a,b)
print('外积(a,b)=\n',d)
基本数学运算:元素对元素运算
数乘:3*a= [3 6 9]
a-b= [ 0.  1.  2.]
a+b= [ 2.  3.  4.]
a*b= [ 1.  2.  3.]
b/a= [ 1.          0.5         0.33333333]
向量运算
点乘(内积)a.*b= 6.0
外积(a,b)=
 [[ 1.  1.  1.]
 [ 2.  2.  2.]
 [ 3.  3.  3.]]

这里要特别特别注意的是,向量的乘法*和除法定义的是元素对元素的乘除法,这点与MATLAB等有很大不同。

除了可以使用numpy.array()创建向量,也可以创建矩阵,创建矩阵时,提供一个列表的列表,每个列表代表矩阵中的一行:

A=np.array([[1,2,3],[4,8,6],[4,3,2]])
print("矩阵A=\n",A)
矩阵A=
 [[1 2 3]
 [4 8 6]
 [4 3 2]]

以及一些特殊矩阵:

M=np.zeros((3,3)) #全为0的矩阵
print("矩阵M=\n",M)
O=np.ones((3,3)) #全为1的矩阵
print("矩阵O=\n",O)
I=np.eye(3) #单位阵
print("矩阵I=\n",I)
矩阵M=
 [[ 0.  0.  0.]
 [ 0.  0.  0.]
 [ 0.  0.  0.]]
矩阵O=
 [[ 1.  1.  1.]
 [ 1.  1.  1.]
 [ 1.  1.  1.]]
矩阵I=
 [[ 1.  0.  0.]
 [ 0.  1.  0.]
 [ 0.  0.  1.]]

以及一些矩阵的运算:

print("A+I=\n",A+I)
print("A-I=\n",A-I)
print("A*I=\n",A*I)
print("I/A=\n",I/A)
print("矩阵相乘,A*O=\n", np.dot(A,O))
print("矩阵相乘,O*A=\n", np.dot(O,A))
print("矩阵转置,tranpose(A)=\n", A.transpose())
print("矩阵的逆,inv(A)=\n", np.linalg.inv(A))
l,L=np.linalg.eig(A)
print("矩阵的特征值=\n", l)
print("矩阵的特征向量=\n", L)
A+I=
 [[ 2.  2.  3.]
 [ 4.  9.  6.]
 [ 4.  3.  3.]]
A-I=
 [[ 0.  2.  3.]
 [ 4.  7.  6.]
 [ 4.  3.  1.]]
A*I=
 [[ 1.  0.  0.]
 [ 0.  8.  0.]
 [ 0.  0.  2.]]
I/A=
 [[ 1.     0.     0.   ]
 [ 0.     0.125  0.   ]
 [ 0.     0.     0.5  ]]
矩阵相乘,A*O=
 [[  6.   6.   6.]
 [ 18.  18.  18.]
 [  9.   9.   9.]]
矩阵相乘,O*A=
 [[  9.  13.  11.]
 [  9.  13.  11.]
 [  9.  13.  11.]]
矩阵转置,tranpose(A)=
 [[1 4 4]
 [2 8 3]
 [3 6 2]]
矩阵的逆,inv(A)=
 [[ 0.06666667 -0.16666667  0.4       ]
 [-0.53333333  0.33333333 -0.2       ]
 [ 0.66666667 -0.16666667  0.        ]]
矩阵的特征值=
 [ 11.80142315  -2.04468118   1.24325803]
矩阵的特征向量=
 [[ 0.26952559  0.61195333  0.44683972]
 [ 0.88453378  0.21152352 -0.72721843]
 [ 0.3807308  -0.76208328  0.52104474]]

同样,乘法*和除法定义的是元素对元素的乘除法,切记!

如果需要提取出矩阵的元素,可以使用 A[row,col](而非A[row][col]!!)

此外,数组和矩阵都支持切片操作。比如,如果希望提取出矩阵A的奇数行、奇数列,可以用:

print("A的奇数行、奇数列=\n", A[::2,::2])
A的奇数行、奇数列=
 [[1 3]
 [4 2]]

另外,这里与Python中的另外一个不同是,在Python的列表中,实行切片操作是直接对切片出的元素进行复制,而在NumPy中,切片操作只是一个视图(view),并没有进行复制。理解这一点是非常关键的,比如:

lista=[1,2,3,4]
suba=lista[1:3]
suba[0]=1
print(lista)
print(suba)
veca=np.array([1,2,3,4])
suba=veca[1:3]
suba[0]=1
print(veca)
print(suba)
[1, 2, 3, 4]
[1, 3]
[1 1 3 4]
[1 3]

因而,在对数组、矩阵进行切片、修改时,需要特别注意。

如果需要创建切片的副本,需要用copy()方法:

veca=np.array([1,2,3,4])
suba=veca[1:3].copy()
suba[0]=1
print(veca)
print(suba)
[1 2 3 4]
[1 3]

可以发现原始向量并没有被改变。

16.2 向量、矩阵的变形、合并和分裂

对于向量、矩阵的另外一个更常用的操作是变形,比如,我们可能将一个向量变形为一个矩阵,可以按照如下的方法:

vec_a=np.linspace(0,8,9)
print("vec_a:\n",vec_a)
mat_a=vec_a.reshape(3,3)
print("mat_a:\n",mat_a)
vec_b=vec_a.reshape(9,1)
print("vec_b:\n",vec_b)
vec_a:
 [ 0.  1.  2.  3.  4.  5.  6.  7.  8.]
mat_a:
 [[ 0.  1.  2.]
 [ 3.  4.  5.]
 [ 6.  7.  8.]]
vec_b:
 [[ 0.]
 [ 1.]
 [ 2.]
 [ 3.]
 [ 4.]
 [ 5.]
 [ 6.]
 [ 7.]
 [ 8.]]

实际上,我们可以通过向量或者矩阵的ndim属性查看其维度:

print("Dimension of vec_a:%2d" % vec_a.ndim)
print("Dimension of vec_b:%2d" % vec_b.ndim)
print("Dimension of mat_a:%2d" % mat_a.ndim)
Dimension of vec_a: 1
Dimension of vec_b: 2
Dimension of mat_a: 2

可见,vec_b作为列向量,在NumPy中实际上看成时一个矩阵。如果要看其维数,需要用shape属性:

print("Dimension of vec_a:%2d" % vec_a.shape)
print("Dimension of vec_b:%2d ×%2d" % vec_b.shape)
print("Dimension of mat_a:%2d ×%2d" % mat_a.shape)
Dimension of vec_a: 9
Dimension of vec_b: 9 × 1
Dimension of mat_a: 3 × 3

也可以只用size属性查看其大小:

print("Dimension of vec_a:%2d" % vec_a.size)
print("Dimension of vec_b:%2d" % vec_b.size)
print("Dimension of mat_a:%2d" % mat_a.size)
Dimension of vec_a: 9
Dimension of vec_b: 9
Dimension of mat_a: 9

此外,有时我们还需要拼接两个矩阵,此时需要使用np.concatenate、np.vstack、np.hstack三个方法。

np.concatenate方法用于拼接向量,比如:

a=np.array([1,2,3])
b=np.array([4,5])
c=np.array([6,7,8,9])
vec=np.concatenate([a,b,c])
print(vec)
[1 2 3 4 5 6 7 8 9]

np.vstack用于竖直拼接矩阵,hstack用于水平拼接举着,比如:

mat_a=np.linspace(0,8,9).reshape(3,3)
mat_b=np.linspace(10,18,9).reshape(3,3)
print("mat_a:\n",mat_a)
print("mat_b:\n",mat_b)
mat_hab=np.hstack([mat_a,mat_b])
print("mat_hab:\n",mat_hab)
mat_vab=np.vstack([mat_a,mat_b])
print("mat_hab:\n",mat_vab)
mat_a:
 [[ 0.  1.  2.]
 [ 3.  4.  5.]
 [ 6.  7.  8.]]
mat_b:
 [[ 10.  11.  12.]
 [ 13.  14.  15.]
 [ 16.  17.  18.]]
mat_hab:
 [[  0.   1.   2.  10.  11.  12.]
 [  3.   4.   5.  13.  14.  15.]
 [  6.   7.   8.  16.  17.  18.]]
mat_hab:
 [[  0.   1.   2.]
 [  3.   4.   5.]
 [  6.   7.   8.]
 [ 10.  11.  12.]
 [ 13.  14.  15.]
 [ 16.  17.  18.]]

16.3 随机数生成

科学计算中很多算法都依赖于随机数的生成,比如数值积分计算的Monte Carlo法、MCMC等方法。在Python中,自带了numpy.random包,可以用来生成随机数。

为了使用numpy.random,必须先导入:

import numpy.random as nprd

接下来,就可以直接使用了。比如,

  • nprd.random(n) 产生一个 n 维向量,每个分量都服从均匀分布的随机数;
  • nprd.randn(n) 产生一个 n 维向量,每个分量都服从正态分布的随机数;
  • nprd.choice(a) 从向量 a 中随机抽取一个元素

……

具体随机数列表可以查看 numpy - random number

16.4 向量函数

在此之前,我们曾经介绍过使用 Python 中自带的 math.exp()math.sin()math.cos() 等函数。现在想象以下,为了计算一个向量每个分量的指数函数,我们不得不写一个循环:

import math
a=np.array([1,2,3])
b=np.array([math.exp(i) for i in a])
print(b)
[  2.71828183   7.3890561   20.08553692]

然而,Python 的循环非常的缓慢,如果数据量巨大,以上做法非常耗时。幸运的是,NumPy 为我们提供了非常强大的函数功能,比如:

a=np.array([1,0.5,3])
print('a=',a)
print('cos(a)=',np.cos(a))
print('exp(a)=',np.exp(a))
print('ln(a)=',np.log(a))
print('abs(a)=',np.abs(a))
a= [ 1.   0.5  3. ]
cos(a)= [ 0.54030231  0.87758256 -0.9899925 ]
exp(a)= [  2.71828183   1.64872127  20.08553692]
ln(a)= [ 0.         -0.69314718  1.09861229]
abs(a)= [ 1.   0.5  3. ]

函数列表可以从 Universal functions (ufunc) 中找到。

此外,NumPy 还提供了方便的数据加总函数,比如求和、平均数、最大值、最小值、中位数等。

比如,以下代码中,我们随机产生了一组正态分布的数据,并计算了其和和、平均数、最大值、最小值、中位数、四分位数:

import numpy as np
import numpy.random as nprd

a = nprd.randn(1000)

print("%-10s%10.4f" % ("和", np.sum(a)))
print("%-10s%10.4f" % ("均值", np.mean(a)))
print("%-10s%10.4f" % ("最大值", np.max(a)))
print("%-10s%10d"   % ("最大值索引", np.argmax(a)))
print("%-10s%10.4f" % ("最小值", np.min(a)))
print("%-10s%10d"   % ("最小值索引", np.argmin(a)))
print("%-10s%10.4f" % ("中位数", np.median(a)))
print("%-10s%10.4f" % ("上四分位数", np.percentile(a, 75)))
print("%-10s%10.4f" % ("下四分位数", np.percentile(a, 25)))
print("%-10s%10.4f" % ("标准差", np.std(a)))
print("%-10s%10.4f" % ("方差", np.var(a)))
和         :   10.9811
均值        :    0.0110
最大值       :    3.6534
最大值索引     :       664
最小值       :   -3.4083
最小值索引     :        91
中位数       :    0.0322
上四分位数     :    0.6480
下四分位数     :   -0.6401
标准差       :    0.9673
方差        :    0.9356

不过,以上代码可能会有问题,如果向量中存在缺失值(比如NaN),以上函数也会计算出NaN。为了避免这一个问题,可以使用以上程序的安全版本:

print("%s%f" % ("和",np.nansum(a)))
print("%s%f" % ("均值",np.nanmean(a)))
print("%s%f" % ("最大值",np.nanmax(a)))
print("%s%f" % ("最大值索引",np.nanargmax(a)))
print("%s%f" % ("最小值",np.nanmin(a)))
print("%s%f" % ("最小值索引",np.nanargmin(a)))
print("%s%f" % ("中位数",np.nanmedian(a)))
print("%s%f" % ("上四分位数",np.nanpercentile(a,75)))
print("%s%f" % ("上四分位数",np.nanpercentile(a,25)))
print("%s%f" % ("标准差",np.nanstd(a)))
print("%s%f" % ("方差",np.nanvar(a)))
和:10.981102
均值:0.010981
最大值:3.653389
最大值索引:664.000000
最小值:-3.408319
最小值索引:91.000000
中位数:0.032241
上四分位数:0.648012
上四分位数:-0.640122
标准差:0.967257
方差:0.935585

此外还有两个特殊的函数:numpy.any()函数用于判断一个逻辑向量(其值为True/False)是否有True;而numpy.all()用于判断逻辑向量是否全为真,比如:

vec_a=np.array([1,2,3])
print((vec_a==2).any())
print((vec_a==5).any())
vec_b=np.array([1,2,3])
print((vec_a==vec_b).all())
vec_b=np.array([1,2,4])
print((vec_a==vec_b).all())
True
False
True
False

16.5 SciPy 简介

SciPyNumPy 的基础扩展了一些用于数值计算的高级工具,比如:

  • scipy.special:特殊函数,包括gamma函数、beta函数、各种统计函数、erf函数等等,都可以从这里找到
  • scipy.optimize:提供了最优化方法
  • scipy.sparse:稀疏矩阵
  • scipy.interpolate:插值
  • scipy.integrate:积分和常微分方程
  • scipy.fftpack:快速傅里叶变换
  • scipy.stats:常用的统计函数

……

在此我们不再赘述,如有需要,可以参考 SciPy User Guide。我们也会在接下来通过实例的方式稍微介绍SciPy的用法。