numpy是python最為常用的庫,沒有之一,它表示Numeric Python,從名字也可以看出來,它被用來做數值計算,常與scipy配合使用。現在幾乎各種應用場合都會用到numpy,主要有以下幾個原因:
numpy提供了很多數值計算和常用算法的函數
numpy歸功了很多線性代數的相關操作
numpy的執行效率高
首先導入numpy庫
import numpy as np
用numpy建的列表類型都是ndarray,因此我們首先來看np.array的用法
np.array的參數列表如下:
numpy.array(object, dtype = None, copy = True, order = None, subok = False, ndmin = 0)
首先來看一維列表的應用
# 構建一維列表
>>> a = np.array([1.0, 2.0, 3.0])
>>> type(a)
<class 'numpy.ndarray'>
>>> a.shape #a是一個一維的array
(3,)
# 列表索引
>>> a[0]
1.0
>>> a[1:3]
array([2., 3.])
>>> a[:2] #默認是0:2
array([1., 2.])
>>> a[0:5:2]
array([1., 3., 5.])
>>> a = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
>>> idx = range(0,5,2)
>>> a[idx]
array([1., 3., 5.])
# 列表的常規運算
# 廣播操作
>>> 3 * a
array([ 3., 6., 9., 12., 15.])
>>> a.sum()
15.0
>>> a.std()
1.4142135623730951
>>> a.cumsum()
array([ 1., 3., 6., 10., 15.])
>>> a**2
array([ 1., 4., 9., 16., 25.])
>>> np.sqrt(a)
array([1. , 1.41421356, 1.73205081, 2. , 2.23606798])
>>> b = np.array([1,2,3,4,5])
>>> a + b
array([ 2., 4., 6., 8., 10.])
>>> a * b
array([ 1., 4., 9., 16., 25.])
>>> 3 * a
>>> a / b
array([1., 1., 1., 1., 1.])
下面再來看二維列表
>>> a = np.array([[1,2,3],[4,5,6]])
>>> print(a)
[[1 2 3]
[4 5 6]]
>>> a.shape
(2, 3)
>>> a = np.random.standard_normal((3,4))
>>> a
array([[-1.75678372, -0.62623838, 0.9360396 , 2.006273 ],
[ 1.1764022 , -0.02253561, 0.28304646, -0.5089298 ],
[-0.60796283, 0.21279179, 0.76809883, 2.03964675]])
# 索引
>>> a[2,3]
2.0396467524421187
>>> a[0:2,1:3]
array([[-0.62623838, 0.9360396 ],
[-0.02253561, 0.28304646]])
>>> a[:2,1:]
array([[-0.62623838, 0.9360396 , 2.006273 ],
[-0.02253561, 0.28304646, -0.5089298 ]])
>>> a[:2,1:-1]
array([[-0.62623838, 0.9360396 ],
[-0.02253561, 0.28304646]])
# 基本運算
>>> a.sum()
3.8998483020681207
>>> a.std()
1.0801601854740654
>>> a.cumsum()
array([-1.75678372, -2.3830221 , -1.4469825 , 0.55929051, 1.73569271,
1.7131571 , 1.99620356, 1.48727376, 0.87931093, 1.09210272,
1.86020155, 3.8998483 ])
在新建ndarray時指定維數
>>> a = np.array([1,2,3,4], ndmin=2)
>>> a
array([[1, 2, 3, 4]])
>>> a.shape # a是一個二維array
(1, 4)
numpy.array()
首先就是np.array了,可以直接把list轉成ndarray
>>> lst = [1,2,3,4]
>>> nd1 = np.array(lst)
>>> nd1
array([1, 2, 3, 4])
numpy.arange(n)
生成range數據,注意要區別於python中的range()函數,numpy中的arange()
>>> nr = np.arange(1,10,2)
>>> nr
array([1, 3, 5, 7, 9])
numpy.ones(shape)
生成全1的array
>>> n1 = np.ones((2,3))
>>> n1
array([[1., 1., 1.],
[1., 1., 1.]])
numpy.zeros(shape)
生成全0的array
>>> n0 = np.zeros((2,3))
>>> n0
array([[0., 0., 0.],
[0., 0., 0.]])
numpy.full(shape, val)
生成數值都是val的array
>>> nv = np.full((2,3), 5)
>>> nv
array([[5, 5, 5],
[5, 5, 5]])
numpy.eye(n)
生成單位矩陣
>>> ne = np.eye(3)
>>> ne
array([[1., 0., 0.],
[0., 1., 0.],
[0., 0., 1.]])
numpy.ones_like(a)
按數組a的形狀和類型生成全1的數組
>>> a = [[1.0,2,3],[4,5,6]]
>>> nol = np.ones_like(a)
>>> nol
array([[1., 1., 1.],
[1., 1., 1.]])
numpy.zeros_like(a)
按數組a的形狀和類型生成全0的數組
numpy.full_like (a, val)
按數組a的形狀和類型生成數值全是val的數組
numpy.linspace(m,n,d)
>>> n1 = np.linspace(0,6,3)
>>> n1
array([0., 3., 6.])
>>> n2 = np.linspace(0,6,3, endpoint=False)
>>> n2
array([0., 2., 4.])
numpy.concatenate()
拼接兩個ndarray
>>> np.concatenate((n1, n2))
array([0., 3., 6., 0., 2., 4.])
也可以指定拼接的方向,按行拼接或者按列拼接,需要注意的是,一維array的拼接不能指定axis=0,axis=0表示按列拼接,axis=1表示按行拼接
>>> n11 = np.array(n1, ndmin=2)
>>> n22 = np.array(n2, ndmin=2)
>>> np.concatenate((n11,n22), axis=1)
array([[0., 3., 6., 0., 2., 4.]])
>>> np.concatenate((n11,n22), axis=0)
array([[0., 3., 6.],
[0., 2., 4.]])
obj.reshape(shape)
>>> a = np.arange(0,6,1).reshape(2,3)
>>> a
array([[0, 1, 2],
[3, 4, 5]])
obj.resize(shape)
跟numpy.reshape()的區別在於resize()沒有返回值,直接對原始數組進行修改
>>> a = np.arange(0,6,1)
>>> a.resize(2,3)
>>> a
array([[0, 1, 2],
[3, 4, 5]])
obj.swapaxes(ax1, ax2)
將兩個維度調換,numpy中的維數從0開始索引,即如果是個二維的array,則維度的索引值為0 1
>>> a.swapaxes(0,1)
array([[0, 3],
[1, 4],
[2, 5]])
obj.flatten()
對數組進行降維,從名字也可以看出,就是把數組打平
>>> a = np.random.standard_normal((2,3))
>>> a
array([[ 0.27694629, 2.13266506, 1.51229221],
[ 0.83796963, -0.61849818, -0.99148708]])
>>> a.flatten()
array([ 0.27694629, 2.13266506, 1.51229221, 0.83796963, -0.61849818,
-0.99148708])
obj.astype(new_type)
>>> a.astype(np.int)
array([[0, 2, 1],
[0, 0, 0]])
obj.tolist()
把ndarray轉換成list
>>> a.tolist()
[[0.27694628837953544, 2.1326650560980402, 1.5122922063725635], [0.8379696303853928, -0.6184981767787137, -0.9914870782615903]]
np.sqrt() #開平方根
np.abs() #求模值
np.square() #取平方
np.log() #自然對數
np.log2() #以2為底的對數
np.log10() #以10為底的對數
np.exp() #指數
np.sign() #取符號
np.maximum(a,b) #最大值
np.minimum(a,b) #最小值
np.fmax() #取最大值,返回ndarray
np.fmin() #取最小值,返回ndarray
np.copysign(a,b) #將b中各元素的符號賦值給數組a的對應元素
np.random.rand(m,n)
生成m行v列[0,1)之間均勻分布的隨機數
>>> np.random.rand(2,3)
array([[0.30555863, 0.41887171, 0.12944243],
[0.047311 , 0.5610462 , 0.05545177]])
np.ramdom.randn(m,n)
生成m行v列服從標準正態分布的隨機數
標準正態分布
>>> np.random.randn(3,4)
array([[ 0.23319176, 0.25340606, 0.68854916, -1.05048334],
[-1.50006282, 0.57744967, -0.32361244, -2.24994722],
[-2.17479774, -0.06314763, -0.41480656, -1.75927588]])
np.random.standard_normal((m,n))
跟np.random.randn(m,n)功能相同,只是參數為tuple
np.random.normal(mean, var, shape)
產生均值為mean,標準差為var,shape形狀的正態分布
>>> np.random.normal(0,1,(3,4))
array([[ 1.33985417, -0.73385786, 1.24840547, 0.383745 ],
[-0.88195275, -1.96785155, -0.22755158, -0.02383854],
[-0.82976081, -0.33360492, -0.85581441, 1.28111454]])
np.ramdom.randint(low,high,(shape))
生成在low和high之間的隨機整數
>>> np.random.randint(1,10,(2,3))
array([[8, 9, 9],
[2, 3, 1]])
np.random.uniform(low, high, shape)
生成在low和high之間的隨機浮點數數
>>> np.random.uniform(1,20,(4,5))
array([[10.30839345, 9.15929767, 2.09133005, 10.90228798, 2.92443925],
[ 3.1389329 , 19.84637209, 2.87868248, 14.43621747, 10.66421098],
[14.21018776, 7.68568737, 12.42300546, 12.73930224, 18.99678353],
[ 1.34370231, 12.82687093, 3.53277008, 14.82696923, 5.52948898]])
np.ramdom.seed(n)
隨機數種子,如果想每次產生的隨機數多是一樣的,只要設置相同的隨機數種子就可以
np.random.shuffle(a)
根據數組a的第一軸進行隨機排列,改變數組a
>>> a = np.random.standard_normal((4,5))
>>> a
array([[ 0.94883904, 0.06622621, -0.55604216, 0.4201526 , 0.93582474],
[-0.19459866, 0.61766597, -0.0381477 , -0.76228097, -0.39149656],
[-0.15616535, 1.69581075, -1.36644287, -1.48007808, -0.45827237],
[-2.10678691, 1.09201772, -1.27128825, 0.47358959, 0.37246547]])
>>> np.random.shuffle(a)
>>> a
array([[-2.10678691, 1.09201772, -1.27128825, 0.47358959, 0.37246547],
[-0.15616535, 1.69581075, -1.36644287, -1.48007808, -0.45827237],
[-0.19459866, 0.61766597, -0.0381477 , -0.76228097, -0.39149656],
[ 0.94883904, 0.06622621, -0.55604216, 0.4201526 , 0.93582474]])
permutation(a)
根據數組a的第一軸進行隨機排列, 但是不改變原數組,將生成新數組
>>> np.random.permutation(a)
array([[ 0.94883904, 0.06622621, -0.55604216, 0.4201526 , 0.93582474],
[-0.15616535, 1.69581075, -1.36644287, -1.48007808, -0.45827237],
[-2.10678691, 1.09201772, -1.27128825, 0.47358959, 0.37246547],
[-0.19459866, 0.61766597, -0.0381477 , -0.76228097, -0.39149656]])
choice(a[, shape, replace, p])
從一維數組a中以概率p抽取元素, 形成shape形狀新數組,replace表示是否可以重用元素,默認為False
>>> a = range(1,10)
>>> np.random.choice(a,(3,2))
array([[5, 2],
[6, 9],
[6, 8]])
np.random.poisson(lambda,shape)
泊松分布
>>> np.random.poisson(10,(2,3))
array([[11, 7, 8],
[ 6, 8, 13]])
np.sum(a, axis = None) #求和
np.mean(a, axis = None) #求均值
np.std(a, axis = None) #求標準差
np.average(a, axis = None, weights) #按權重求均值
np.min(a) #最小值
np.max() #最大值
np.argmin() #最小值下標
np.argmax() #最大值下標
np.unravel_index(index, shape) #根據shape將一維下標index轉成多維下標
np.median(a) #中值
np.ptp(a) #最大值和最小值的差
np.gradient(a)
計算數組a中元素的梯度,f為多維時,返回每個維度的梯度
>>> a = np.random.randn(2,3)
>>> np.gradient(a)
[array([[-0.40790034, -0.85434474, 0.78626434],
[-0.40790034, -0.85434474, 0.78626434]]), array([[ 1.72286723, 0.14257516, -1.43771691],
[ 1.27642283, 0.7396575 , 0.20289217]])]
pandas的用法我們之前整理過,它對DataFrame的處理非常方便,但pandas運行的確實太慢了,如果是一些簡單的DataFrame,我們可以使用numpy的結構數組來替代,同樣簡單方便,運行還快
>>> dt = np.dtype([('name', 'S10'), ('age', '<i4'), ('height', 'f'), ('child/adult', 'i4', 2)])
>>> data = np.array([('Jim', 20, 178, (0, 1)),('John', '5', 90, (1, 0))],dtype = dt)
>>> data
array([(b'Jim', 20, 178., [0, 1]), (b'John', 5, 90., [1, 0])],
dtype=[('name', 'S10'), ('age', '<i4'), ('height', '<f4'), ('child/adult', '<i4', (2,))])
>>> data['name']
array([b'Jim', b'John'], dtype='|S10')
>>> data['age'].mean()
12.5
數據類型的表示:
i: 32bit的整數類型, 相當於np.int32
f: 32bit的單精度浮點數, 相當於np.float32
|: 忽視字節順序
<: 低位字節在前, 即大端序
>: 高位字節在前, 即小端序
b1, i1, i2, i4, i8, u1, u2, u4, u8, f2, f4, f8, c8, c16, a
int8,…,uint8,…,float16, float32, float64, complex64, complex128 (這裡是按位長計算bit sizes)
代碼向量化用過向量的朋友應該都知道,向量化運算,別提有多爽,不用一層一層的for循環了,python本身是不能夠向量化運算的,但numpy卻可以。
>>> r=np.random.standard_normal((4, 3))
>>> s=np.random.standard_normal((4, 3))
>>> s + r
array([[-1.511067 , 0.19941485, -3.34742021],
[ 1.11920413, 0.10829615, -1.79163394],
[ 0.0605728 , -0.50659722, 1.08233105],
[ 1.10578534, -1.22821669, -0.65957104]])
>>> 2*r + 3
array([[ 0.25647939, 1.50448199, 0.27938363],
[ 4.77064689, 1.03096248, -0.9815989 ],
[ 1.24174253, 3.8165197 , 3.22864982],
[ 2.63337107, 1.08923183, 0.0950697 ]])
>>> r.transpose()
array([[-1.37176031, 0.88532344, -0.87912874, -0.18331447],
[-0.74775901, -0.98451876, 0.40825985, -0.95538409],
[-1.36030818, -1.99079945, 0.11432491, -1.45246515]])
>>> np.shape(r)
(4, 3)
對於數學函數,math庫是不支持向量化運算的,此時可以用numpy中的數學函數
>>> import math
>>> math.sin(r)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: only size-1 arrays can be converted to Python scalars
>>> np.sin(r)
array([[-0.98025764, -0.67999734, -0.97792904],
[ 0.77411978, -0.83300593, -0.91308767],
[-0.77018346, 0.3970128 , 0.11407603],
[-0.1822895 , -0.81653553, -0.99300703]])
>>> np.sin(np.pi)
1.2246467991473532e-16
我們都知道numpy的運行速度要快很多,下面我們來直觀的感受一下
import time
from functools import reduce
import random
I=5000
time_st1 = time.time()
mat=[[random.gauss(0, 1) for j in range(I)] for i in range(I)]
reduce(lambda x, y: x + y, [reduce(lambda x, y:x + y, row) for row in mat
time_end1 = time.time()
time_st2 = time.time()
mat=np.random.standard_normal([I, I])
mat.sum()
time_end2 = time.time()
print(time_end1 - time_st1)
print(time_end2 - time_st2)
> 28.042604207992554
> 1.446082592010498
可以看出,使用numpy要節省很多的時間,下面我們看為什麼numpy的速度快?
效率提高進階python是動態類型,而非靜態類型
像C這種靜態類型的語言,在一開始編譯器就知道所有的數據類型,按照下面的流程進行處理。
int a = 1;
int b = 2;
int c = a + b;
Python的解釋器只有在運行的時候才會確定變量的類型,解釋器會對每個變量進行檢查,然後才進行賦值操作。
可以看出,一個簡單的加法python就比C要多了好多步,這是python慢的第一個原因。
編譯器(compiler)比解釋器(interpreter)更快的地方在於編譯器往往會把很多重複的或者沒有用到的操作進行優化,在運行的時候就會更快一些。
python運行慢還有一個重要原因就是python存放數據時往往不是在連續區域,這樣就導致數據的索引效率不高。而numpy則採用了C的邏輯,將np.array數據放在連續區域,提高批量數據的讀寫速度。
如何正確的使用numpy知道了numpy運行快的原因了,那我們怎麼使用numpy才能達到加速的效果呢?
內存數據存儲形式
在np.array中我們經常會用到2D或者多維的數據,但內存中沒有維度這個概念,就是存儲在連續空間中。我們創建 ndArray 的方式不同, 在這個連續空間上的排列順序也有不同,我們採用不同方式進行讀寫的速度也會不同,使用了numpy後發現速度沒有提升,多半的原因都是因為對數據的讀寫方式的問題。
Numpy默認與C的存儲方式相同,即按行排列,當然我們也可以指定numpy的內存存儲方式,當存儲方式確定後,再用對應的方法去讀寫數據,速度就會明顯提升。(在.py文件中用time.time()查看運行時間時,運行一次往往不太準確,需要運行多次,看平均時間或者最小最大時間;也可以在ipython(注意是ipython,不是python console)中使用%timeit命令,可以自動給出運行多次後的最小運行時間)。下表是用%timeit統計出來的計算時間。
x1 = np.ones((5,10000000))
C1 = np.array(x,order = 'C')
F2 = np.array(x,order = 'F')
x2 = np.ones((10000000,5))
C2 = np.array(x,order = 'C')
F2 = np.array(x,order = 'F')
x3 = np.ones((1000,1000))
C3 = np.array(x,order = 'C')
F3 = np.array(x,order = 'F')
從運行結果可以看出,對行做運行的速度以C形式排列要快於以F形式排列,而對列做運算時,以F形式排列比C形式排列速度要快。原理也很容易理解,哪一種方式可以對內存的連續地址的數據做處理,哪一種的速度就快。當一個數據中行數明顯多與列數時,對該數據的處理時C要快於F;當一個數據中列數明顯多與行數時,對該數據的處理時F要快於C。
view跟copy
copy就是從內存中將數據拷貝到另一個地方,view就是直接對原始數據做處理。類似於我們前面講的數字跟列表的區別。
>>> a = np.array([1,2,3,4])
>>> a_view = a
>>> a_copy = a.copy()
>>> a[0] = 5
>>> a_view
array([5, 2, 3, 4])
>>> a_copy
array([1, 2, 3, 4])
在numpy中,有幾種結果相同但處理過程不同的操作,而這些操作的運行速度也不盡相同。
View不會複製東西,速度快,而copy則速度慢。在numpy中,a=2是用view方式,而a=a2則是用copy的方式。
N = 100
a = np.random.standard_normal((1000000,1))
def func1(a,N):
for i in range(N):
a = a*2
def func2(a,N):
for i in range(N):
a *= 2
t0 = time.time()
func1(a,N)
t1= time.time()
func2(a,N)
t2= time.time()
print(t1-t0)
> 0.5090291500091553
print(t2-t1)
> 0.0820047855377197
講NpArray矩陣中的數據打平時,用ravel()要比flatten()要快,也是因為ravel()僅在需要copy的時候才會進行copy,不進行copy時會使用view的方式。
a = np.random.standard_normal((1000,1000))
%timeit a.flatten()
> 3.31 ms ± 155 µs per loop
%timeit a.ravel()
> 282 ns ± 5.02 ns per loop
選擇數據
總的來說,用slice方式選擇數據比用index方式更快一些。
a_view1 = a[1:2, 3:6] #
a_view2 = a[:100] #
a_view3 = a[::2] #
a_view4 = a.ravel() #
a_copy1 = a[[1,4,6], [2,4,6]] # 用 index 選
a_copy2 = a[[True, True], [False, True]] # 用 mask
a_copy3 = a[[1,2], :] # 雖然 1,2 的確連在一起了, 但是他們確實是 copy
Slice用的是view的方式,而index用的是copy方式。
Out參數
使用Out參數也可以加快程序的速度
np.add(a,1)
np.add(a,1,out = a)
np.mul(a,2,out = a)
下面的連結是所有可以使用Out參數的函數
連結 | 可使用Out參數的函數
用numpy代替pandas
連結 | 文章中提出,在數據量比較小時,pandas的效率不如numpy(在實際的使用中,會發現pandas明顯比numpy要慢很多),對於一些簡單的數據結構,可以使用numpy來代替pandas使用。
numpy的結構數組的用法我們上面也提到過,這裡做個運算時間的對比。
aa = pd.DataFrame({'ints':[1,2,3,4], 'floats':[1.1,2.1,3.1,4.1]}, index = ['a','b','c','d'])
dt = np.dtype([('floats','f'),('ints','i4')])
bb = np.array([(1.1,1),(2.1,2),(3.1,3),(4.1,4)],dtype = dt)
%timeit aa['ints']+1
> 112 µs ± 2.03 µs per loop
%timeit bb['ints']+1
> 1.2 µs ± 68.9 ns per loop
可以看出,numpy比pandas的速度要快很多。
既然講到了加速運算,我們就再講一個Python中更簡單的加速方式--numba
numpy是效率雖高,但使用時需要注意數據的排列方式,如果使用不當,是不會起到加速作用的。而numba的用法則更加簡單直接。
numba庫使用jit(just-in-time)加速python低效的for語句,前面我們提到過C比python快的一個原因是C會先編譯好再運行,而jit的原理就是先編譯python,讓代碼變得靜態,從而使運行速度更快。
import numba as nb
@nb.jit
def add_nb(lst):
res = [0] * len(lst)
for i, ele in enumerate(lst):
res[i] = ele + 1
return res
lst = list(range(100000))
>>> %timeit add_nb.__wrapped__(lst) #11.9ms
>>> %timeit add_nb(lst) #5.08ms
__wrapped__是接觸裝飾器作用,這裡的裝飾器就是@nb.jit,當指定__wrapped__後,按沒有jit的函數運行。
可以看出,使用了jit後速度變快。
需要注意的是:
def f():
return [x for x in range(1000)]
如果用numpy進行操作,發現運行時間只有57us。
def add_np(np_array):
return np_array + 3
np_array = np.arange(100000)
nb中還有parallel的加速方式,但很多時候都達不到理想的效果,這裡不做講解。
Numba的jit中還有種多線程加速方式,後續我們會一一講到。