向量化的数组运算比纯python同等程度的运算要快很多。
一个简单的例子,假设我们想要评价函数sqrt(x^2 + y^2)
。np.meshgrid
函数取两个1维的数组,产生一个2维的矩阵,对应于所有两个数组中(x, y)的组合:
import numpy as np
先看个栗子,说明meshgrid
的效果。meshgrid
函数用两个坐标轴上的点在平面上画网格。用法:
[X,Y]=meshgrid(x,y)
[X,Y]=meshgrid(x)
与[X,Y]=meshgrid(x,x)
是等同的[X,Y,Z]=meshgrid(x,y,z)
生成三维数组,可用来计算三变量的函数和绘制三维立体图这里,主要以[X,Y]=meshgrid(x,y)
为例,来对该函数进行介绍。
[X,Y] = meshgrid(x,y)
将向量(x,y)定义的区域转换成矩阵X和Y,其中矩阵X的行向量是向量x的简单复制,而矩阵Y的列向量是向量y的简单复制(注:下面代码中X和Y均是数组,在文中统一称为矩阵了)。
假设x是长度为m的向量,y是长度为n的向量,则最终生成的矩阵X和Y的维度都是 nm (注意不是mn)。
>>>m, n = (5, 3)
>>>x = np.linspace(0, 1, m)
>>>y = np.linspace(0, 1, n)
>>>X, Y = np.meshgrid(x, y)
>>>x
array([0. , 0.25, 0.5 , 0.75, 1. ])
>>>y
array([0. , 0.5, 1. ])
>>>X
array([[0. , 0.25, 0.5 , 0.75, 1. ],
[0. , 0.25, 0.5 , 0.75, 1. ],
[0. , 0.25, 0.5 , 0.75, 1. ]])
>>>Y
array([[0. , 0. , 0. , 0. , 0. ],
[0.5, 0.5, 0.5, 0.5, 0.5],
[1. , 1. , 1. , 1. , 1. ]])
可以看到X和Y的shape都是3x5,用图的话更好理解:
把X和Y画出来后,就可以看到网格了:
>>>import matplotlib.pyplot as plt
>>>%matplotlib inline
>>>plt.style.use('ggplot')
>>>plt.plot(X, Y, marker='.', color='blue', linestyle='none')
[<matplotlib.lines.Line2D at 0x1d2a83192c8>,
<matplotlib.lines.Line2D at 0x1d2a8328648>,
<matplotlib.lines.Line2D at 0x1d2a8328048>,
<matplotlib.lines.Line2D at 0x1d2a8328808>,
<matplotlib.lines.Line2D at 0x1d2a8328a08>]
可以用zip
得到网格平面上坐标点的数据:
>>>z = [i for i in zip(X.flat, Y.flat)]
>>>z
[(0.0, 0.0),
(0.25, 0.0),
(0.5, 0.0),
(0.75, 0.0),
(1.0, 0.0),
(0.0, 0.5),
(0.25, 0.5),
(0.5, 0.5),
(0.75, 0.5),
(1.0, 0.5),
(0.0, 1.0),
(0.25, 1.0),
(0.5, 1.0),
(0.75, 1.0),
(1.0, 1.0)]
下面是书中内容
>>>points = np.arange(-5, 5, 0.01) # 1000 equally spaced points
>>>xs, ys = np.meshgrid(points, points) #xs, ys是一样的
>>>ys
array([[-5. , -5. , -5. , ..., -5. , -5. , -5. ],
[-4.99, -4.99, -4.99, ..., -4.99, -4.99, -4.99],
[-4.98, -4.98, -4.98, ..., -4.98, -4.98, -4.98],
...,
[ 4.97, 4.97, 4.97, ..., 4.97, 4.97, 4.97],
[ 4.98, 4.98, 4.98, ..., 4.98, 4.98, 4.98],
[ 4.99, 4.99, 4.99, ..., 4.99, 4.99, 4.99]])
>>>z = np.sqrt(xs ** 2 + ys ** 2)
>>>z
array([[7.07106781, 7.06400028, 7.05693985, ..., 7.04988652, 7.05693985,
7.06400028],
[7.06400028, 7.05692568, 7.04985815, ..., 7.04279774, 7.04985815,
7.05692568],
[7.05693985, 7.04985815, 7.04278354, ..., 7.03571603, 7.04278354,
7.04985815],
...,
[7.04988652, 7.04279774, 7.03571603, ..., 7.0286414 , 7.03571603,
7.04279774],
[7.05693985, 7.04985815, 7.04278354, ..., 7.03571603, 7.04278354,
7.04985815],
[7.06400028, 7.05692568, 7.04985815, ..., 7.04279774, 7.04985815,
7.05692568]])
这里我们用matplotlib把图画出来:
>>>import matplotlib.pyplot as plt
>>>plt.imshow(z, cmap=plt.cm.gray); plt.colorbar()
>>>plt.title("Image plot of $\sqrt{x^2 + y^2}$ for a grid of values")
Text(0.5, 1.0, 'Image plot of $\\sqrt{x^2 + y^2}$ for a grid of values')
>>>plt.draw()
>>>plt.close('all')
numpy.where
函数是一个向量版的三元表达式,x if condition else y
。假设我们有一个布尔数组和两个数组:
>>>xarr = np.array([1.1, 1.2, 1.3, 1.4, 1.5])
>>>yarr = np.array([2.1, 2.2, 2.3, 2.4, 2.5])
>>>cond = np.array([True, False, True, True, False])
假设如果cond中为true,取xarr中对应的值,否则就取yarr中的值。列表表达式的话会这么写:
>>>result = [(x if c else y)
for x, y, c in zip(xarr, yarr, cond)]
>>>result
[1.1, 2.2, 1.3, 1.4, 2.5]
这么做的话会有很多问题。首先,对于很大的数组,会比较慢。第二,对于多维数组不起作用。但np.where
能让我们写得更简洁:
>>>result = np.where(cond, xarr, yarr)
>>>result
array([1.1, 2.2, 1.3, 1.4, 2.5])
np.where
中第二个和第三个参数不用必须是数组。where在数据分析中一个典型的用法是基于一个数组,产生一个新的数组值。假设我们有一个随机数字生成的矩阵,我们想要把所有的正数变为2,所有的负数变为-2。用where的话会非常简单:
>>>arr = np.random.randn(4, 4)
>>>arr
array([[-0.86617612, 0.20749266, 0.79432893, 1.37933292],
[ 0.16468301, 1.33738792, 0.48931202, -1.93456647],
[-1.04318554, 0.35130318, 1.93013893, -0.49548614],
[ 0.06879538, 0.14403184, -0.6829035 , -0.15724614]])
>>>arr > 0
array([[False, True, True, True],
[ True, True, True, False],
[False, True, True, False],
[ True, True, False, False]])
>>>np.where(arr > 0, 2, -2)
array([[-2, 2, 2, 2],
[ 2, 2, 2, -2],
[-2, 2, 2, -2],
[ 2, 2, -2, -2]])
我们可以结合标量和数组。比如只把整数变为2,其他仍未原来的数字:
>>>np.where(arr > 0, 2, arr) # set only positive values to 2
array([[-0.86617612, 2. , 2. , 2. ],
[ 2. , 2. , 2. , -1.93456647],
[-1.04318554, 2. , 2. , -0.49548614],
[ 2. , 2. , -0.6829035 , -0.15724614]])
一些能计算统计值的数学函数能基于整个数组,或者沿着一个axis(轴)。可以使用aggregations(often called reductions,汇总,或被叫做降维),比如sum
, mean
, std
(标准差).
下面是一些aggregate statistics(汇总统计):
>>>arr = np.random.randn(5, 4)
>>>arr
array([[-0.65977766, 0.10657405, -0.30306886, 1.6113779 ],
[ 1.38028712, 0.19333709, 0.27204392, 0.57891583],
[-1.68330481, -1.13381567, 2.40248441, 0.15887473],
[ 1.14870515, 0.21836443, 0.48274068, -0.49030261],
[ 0.03152238, 1.95722114, 1.15613118, -1.28873975]])
>>>arr.mean()
0.30697853186336255
>>>np.mean(arr)
0.30697853186336255
>>>arr.sum()
6.139570637267251
mean, sum这样的函数能接受axis作为参数来计算统计数字,返回的结果维度更少:
>>>arr.mean(axis=1)
array([ 0.18877635, 0.60614599, -0.06394034, 0.33987691, 0.46403374])
>>>arr.sum(axis=0)
array([0.21743217, 1.34168105, 4.01033133, 0.57012609])
这里arr.mean(1)
表示,compute mean acros the rows(计算各行之间的平均值)。arr.sum(0)
表示,compute sum down the columns(计算各行总和)。
其他一些方法,像cumsum
和cumprod
不做汇总,而是产生一个中间结果的数组:
>>>arr = np.array([0, 1, 2, 3, 4, 5, 6, 7])
>>>arr.cumsum() #累加
array([ 0, 1, 3, 6, 10, 15, 21, 28], dtype=int32)
上面的计算是一个累加的结果,0+1=1,1+2=3,3+3=6
以此类推。
>>>arr = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
>>>arr
array([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
>>>arr.cumsum(axis=0)
array([[ 0, 1, 2],
[ 3, 5, 7],
[ 9, 12, 15]], dtype=int32)
>>>arr.cumprod(axis=1)
array([[ 0, 0, 0],
[ 3, 12, 60],
[ 6, 42, 336]], dtype=int32)
数组的基本统计方法
方法 | 说明 |
---|---|
sum |
对数组中全部或某轴上的元素求和。零长度的数组sum为0 |
mean |
算术平均数。零长度的数组mean为NaN |
std 、var |
分别为标准差和方差,自由度可调(默认为n) |
min 、max |
最大值和最小值 |
argmin 、argmax |
分别为最大和最小元素的索引 |
cumsum |
所有元素累计和 |
cumprod |
所有元素累计积 |
sum对象是布尔型数组时,它是用来计算布尔数组中有多少个true的:
>>>arr = np.random.randn(100)
>>>(arr > 0).sum() # Number of positive values
46
有两个其他方法,any和all,对于布尔数组特别有用。any检测数组中只要有一个ture返回就是true,而all检测数组中都是true才会返回true。
>>>bools = np.array([False, False, True, False])
>>>bools.any()
True
>>>bools.all()
False
numpy中也有sort方法:
>>>np.random.randn? #返回符合正态分布的数值
>>>arr = np.random.randn(6)
>>>arr
array([-1.47806717, -0.13365593, 0.58858679, -2.3985419 , -0.96422824,
0.04325962])
>>>arr.sort()
>>>arr
array([-2.3985419 , -1.47806717, -0.96422824, -0.13365593, 0.04325962,
0.58858679])
如果是多维数组,还可以按axis来排序:
>>>arr = np.random.randn(5, 3)
>>>arr
array([[ 0.78097059, -0.72495837, 0.73126231],
[ 0.03407071, 1.15173636, 0.03578452],
[-0.89827663, 2.26539341, -0.68835086],
[ 0.15338572, 0.47898484, 0.47362358],
[ 0.4391983 , 0.30683821, 0.97400495]])
>>>arr.sort(1)
>>>arr
array([[-0.72495837, 0.73126231, 0.78097059],
[ 0.03407071, 0.03578452, 1.15173636],
[-0.89827663, -0.68835086, 2.26539341],
[ 0.15338572, 0.47362358, 0.47898484],
[ 0.30683821, 0.4391983 , 0.97400495]])
上面是直接调用数组的sort方法,会改变原有数组的顺序。但如果使用np.sort()
函数的话,会生成一个新的排序后的结果。
一个计算分位数的快捷方法是先给数组排序,然后选择某个排名的值:
>>>large_arr = np.random.randn(1000)
>>>large_arr.sort()
>>>large_arr[int(0.05 * len(large_arr))] # 5% quantile
-1.5547315605495498
Numpy也有一些基本的集合操作用于一维数组。np.unique
,能返回排好序且不重复的值:
>>>names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
>>>np.unique(names)
array(['Bob', 'Joe', 'Will'], dtype=')
>>>ints = np.array([3, 3, 3, 2, 2, 1, 1, 4, 4])
>>>np.unique(ints)
array([1, 2, 3, 4])
如果用纯python代码来实现的话,要这么写:
>>>sorted(set(names))
['Bob', 'Joe', 'Will']
np.in1d
, 测试一个数组的值是否在另一个数组里,返回一个布尔数组:
>>>values = np.array([6, 0, 0, 3, 2, 5, 6])
>>>np.in1d(values, [2, 3, 6])
array([ True, False, False, True, True, False, True])
数组的集合运算
方法 | 说明 |
---|---|
unique(x) | 计算x中的唯一元素,并返回有序结果 |
intersect1d(x, y) | 计算x和y中公共元素,并返回有序结果 |
union1d(x, y) | 计算x和y的并集,并返回有序结果 |
in1d(x, y) | 得到一个表示“x的元素是否包含于y”的布尔型数组 |
setdiff1d(x, y) | 集合的差,即元素在x中且不再y种 |
setxor1d(x, y) | 集合的对称差,即存在于一个数组中但不同时存在于两个数组中的元素 |
Numpy能从磁盘直接存储和加载数据,不论是文本格式还是二进制模式。这里我们只考虑Numpy的二进制模式,因为大多数用户更喜欢用pandas或其他工具来加载text或tabular数据。
np.save
和np.load
。数组会以未压缩的原始二进制模式被保存,后缀为.npy
:
>>>import numpy as np
>>>arr = np.arange(10)
>>>np.save('some_array', arr)
即使保存的时候没有加后缀,也会被自动加上。可以用np.load
来加载数组。
>>>np.load('some_array.npy')
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
用np.savez
能保存多个数组,还可以指定数组对应的关键字,不过是未压缩的npz格式:
>>>np.savez('array_archive.npz', a=arr, b=arr)
加载.npz
文件的时候,得到一个dict object:
>>>arch = np.load('array_archive.npz')
>>>arch['b']
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
可以用np.savez_compressed
来压缩文件:
>>>np.savez_compressed('arrays_compressed.npz', a=arr, b=arr)
在MATLAB里,*
代表矩阵乘法。但是在numpy里,*
表示element-wise prodct。要想做到矩阵乘法,要用多函数dot
:
>>>import numpy as np
>>>x = np.array([[1., 2., 3.], [4., 5., 6.]])
>>>y = np.array([[6., 23.], [-1, 7], [8, 9]])
>>>x
array([[1., 2., 3.],
[4., 5., 6.]])
>>>y
array([[ 6., 23.],
[-1., 7.],
[ 8., 9.]])
>>>x.dot(y)
array([[ 28., 64.],
[ 67., 181.]])
x.dot(y)
等同于np.dot(x, y)
:
>>>np.dot(x, y)
array([[ 28., 64.],
[ 67., 181.]])
一个二维数组和一个一维数组的矩阵乘法,得到一个一维数组:
>>>np.dot(x, np.ones(3))
array([ 6., 15.])
@
作为一个中缀计算符,也能实现矩阵乘法:
>>>x @ np.ones(3)
array([ 6., 15.])
np.linalg
能用来做矩阵分解,以及比如转置和求秩之类的事情:
>>>from numpy.linalg import inv, qr
>>>X = np.random.randn(5, 5)
#X = np.round(np.random.randn(5, 5), 3) #这里我们用np.round控制小数点后的位数,看起来更舒服一些
>>>X
array([[-1.21810897, -0.29265147, -0.17907474, -0.24168411, -2.25091962],
[-0.58843199, -0.61295374, 0.59243325, 0.6684168 , 0.32061682],
[-0.18505361, 0.93213111, -1.77880663, 1.23613944, 0.42735645],
[ 0.94444125, -0.76661742, -2.40385328, 0.08920931, -0.33557356],
[-1.47261914, 0.05994258, -1.54126795, -0.08375278, -1.52619611]])
>>>mat = X.T.dot(X) #X转置乘以X
>>>np.round(mat, 2)
array([[ 4.92, -0.27, 0.2 , -0.12, 4.4 ],
[-0.27, 1.92, -0.22, 0.74, 1.03],
[ 0.2 , -0.22, 11.7 , -1.84, 2.99],
[-0.12, 0.74, -1.84, 2.05, 1.38],
[ 4.4 , 1.03, 2.99, 1.38, 7.79]])
>>>np.round(inv(mat), 2)
array([[ 1.19, 0.39, 0.42, 1.03, -1.07],
[ 0.39, 0.75, 0.13, 0.13, -0.39],
[ 0.42, 0.13, 0.29, 0.55, -0.46],
[ 1.03, 0.13, 0.55, 1.75, -1.12],
[-1.07, -0.39, -0.46, -1.12, 1.16]])
>>>np.round(mat.dot(inv(mat)), 2)
array([[ 1., 0., 0., 0., 0.],
[ 0., 1., 0., -0., 0.],
[-0., -0., 1., -0., 0.],
[-0., -0., -0., 1., 0.],
[-0., -0., 0., 0., 1.]])
>>>q, r = qr(mat)
>>>np.round(r, 2)
array([[ -6.62, -0.39, -2.53, -0.71, -8.49],
[ 0. , -2.29, 1. , -1.97, -2.56],
[ 0. , 0. , -11.91, 1.78, -3.15],
[ 0. , 0. , 0. , -1.59, -1.81],
[ 0. , 0. , 0. , 0. , 0.49]])
X.T.dot(X)
计算的是X和X的转置的矩阵乘法。
一些常用的numpy.linalg函数
函数 | 描述 |
---|---|
diag | 以一维数组的形式返回方阵的对角线(或非对角线)元素, 或将一维数组转换为方阵(非对角线元素为零) |
dot | 矩阵乘法 |
trace | 计算对角线元素和(即矩阵的迹) |
det | 计算方阵行列式 |
eig | 计算方阵特征值和特征向量 |
inv | 计算矩阵的逆 |
pinv | 计算矩阵的Moore-Penrose伪逆 |
qr | 计算QR分解 |
svd | 计算奇异值分解(SVD) |
solve | 解线性方程组Ax=b,其中A为一个方阵 |
lstsq | 计算Ax=b的最小二乘解 |
numpy.random
模块提供了很多生成随机数的函数,可以选择生成符合某种概率分布的随机数。比如我们可以用normal
得到一个4 x 4的,符合标准正态分布的数组:
>>>import numpy as np
>>>samples = np.random.normal(size=(4, 4))
>>>samples
array([[ 0.40335566, 0.49012624, 0.51915573, 4.03131376],
[ 1.14935054, -0.33330548, -0.31231616, 0.40892981],
[-1.5456503 , 0.52125239, -0.37211993, 1.18562307],
[-1.67826879, -0.91838905, 0.65828423, 0.65344736]])
相对的,python内建的random模块一次生成一个样本。在生成大量样本方法,numpy.random是非常快的:
>>>from random import normalvariate
>>>N = 1000000
>>>%timeit samples = [normalvariate(0, 1) for _ in range(N)]
1.19 s ± 112 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
>>>%timeit np.random.normal(size=N)
38.1 ms ± 3.75 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
之所以称之为伪随机数,是因为随机数生成算法是根据seed来生成的。也就是说,只要seed设置一样,每次生成的随机数是相同的:
>>>np.random.seed(1234)
当然,这个seed是全局的,如果想要避免全局状态,可以用numpy.random.RandomState
来创建一个独立的生成器:
>>>rng = np.random.RandomState(1234)
>>>rng.randn(10)
array([ 0.47143516, -1.19097569, 1.43270697, -0.3126519 , -0.72058873,
0.88716294, 0.85958841, -0.6365235 , 0.01569637, -2.24268495])
numpy.random
部分函数表
函数 | 描述 |
---|---|
seed | 确定随机数生成器的种子 |
permutation | 返回一个序列的随机排列,或返回一个排列范围 |
shuffle | 对一个序列随机排序 |
rand | 从均匀分布中抽取样本 |
randint | 从给定的上下限范围内抽取整数 |
randn | 产生标准正态分布样本值 |
binomial | 产生二项分布样本值 |
normal | 产生正态(高斯)分布样本值 |
beta | 产生Beta分布样本值 |
chisquare | 产生卡方分布样本值 |
gamma | 产生Gamma分布样本值 |
uniform | 产生[0,1]均匀分布样本值 |
这个例子让我了解一个在实际任务中如何利用数组操作。首先一个最简单的随机漫步:从0开始,步幅为1和-1,以相同的概率出现。
下面是纯python的实现方法,1000步:
>>>import random
>>>position = 0
>>>walk = [position]
>>>steps = 1000
>>>for i in range(steps):
>>> step = 1 if random.randint(0, 1) else -1
>>> position += step
>>> walk.append(position)
>>>plt.figure()
<Figure size 432x288 with 0 Axes>
<Figure size 432x288 with 0 Axes>
>>>plt.plot(walk[:100])
[<matplotlib.lines.Line2D at 0x1a0f9d34a08>]
其中随机游动的前100个值的示例图:
随机漫步其实就是一个简单的累加。而用np.random能更快:
>>>import numpy as np
>>>np.random.seed(12345)
>>>nsteps = 1000
>>>draws = np.random.randint(0, 2, size=nsteps)
>>>steps = np.where(draws > 0, 1, -1)
>>>walk = steps.cumsum()
我们能直接从中得到一些统计数据,比如最大值和最小值:
>>>walk.min()
-5
>>>walk.max()
24
一个更复杂的统计值是在哪一步random walk到达了一个指定值。我们想知道从0走出10步用了多久,不论是正方向还是负方向。np.abs(walk) >= 10
给我们一个布尔数组,walk已经到达或超过10的位置,但是我们需要第一个10或-10的索引。因此,可以使用argmax来计算,它返回布尔数组中最大值的第一个索引(True是最大值):
>>>(np.abs(walk) >= 10).argmax()
119
注意,使用argmax并不总是高效的,因为它总会搜索整个数组。在这里例子里,一旦True被找到了,我们就返回为最大值。
>>>nwalks = 5000
>>>nsteps = 1000
>>>draws = np.random.randint(0, 2, size=(nwalks, nsteps)) # 0 or 1
>>>steps = np.where(draws > 0, 1, -1)
>>>walks = steps.cumsum(1)
>>>walks
array([[ 1, 2, 3, ..., 50, 51, 50],
[ -1, 0, -1, ..., -12, -11, -10],
[ -1, 0, 1, ..., 10, 9, 8],
...,
[ -1, 0, 1, ..., 48, 47, 48],
[ -1, 0, 1, ..., -28, -29, -28],
[ -1, 0, 1, ..., 72, 71, 72]], dtype=int32)
找到所有漫步中的最大值和最小值:
>>>walks.max()
108
>>>walks.min()
-119
在这些漫步模拟中,我们想找到30步以上的。用any
方法:
>>>hits30 = (np.abs(walks) >= 30).any(1)
>>>hits30
array([ True, False, False, ..., True, True, True])
>>>hits30.sum() # Number that hit 30 or -30
3353
然后我们利用这个布尔型数组选出那些穿越了30(绝对值)的随机漫步(⾏),并调⽤argmax在轴1上获取穿越时间:
>>>crossing_times = (np.abs(walks[hits30]) >= 30).argmax(1)
>>>crossing_times.mean()
504.5872353116612
参考资料:
利用Python进行数据分析学习笔记(有惊喜
^_^
)书籍:《Python for Data Analysis》