1. 程式人生 > 其它 >Numpy學習 第04章 NumPy基礎:陣列和向量計算

Numpy學習 第04章 NumPy基礎:陣列和向量計算

前言

正式開始學習Numpy,參考用書是《用Python進行資料清洗》,計劃本週五之前把本書讀完,關鍵程式碼全部實現一遍

NumPy基礎:陣列和向量計算

按照書中所示,要搞明白具體的效能差距,考察一個包含一百萬整數的陣列,和一個等價的Python列表:

import numpy as np
my_arr = np.arange(1000000)
my_list = list(range(1000000))

各個序列分別乘以2:

%time for _ in range(10): my_arr2 = my_arr * 2
CPU times: user 20 ms, sys: 50 ms, total: 70 ms
Wall time: 72.4 ms

%time for _ in range(10): my_list2 = [x * 2 for x in my_list]
CPU times: user 760 ms, sys: 290 ms, total: 1.05 s
Wall time: 1.05 s

問題

此處出現報錯

%time for _ in range(10): my_arr2 = my_arr * 2
    ^
SyntaxError: invalid syntax

據查閱,原來這不是python的語法,是ipython的特殊功能,用於測試語句執行的時間。於是cmd中直接啟動ipython,再執行

In [4]: %time for _ in range(10): my_arr2 = my_arr * 2
Wall time: 15.7 ms

In [5]: %time for _ in range(10): my_list2 = [x * 2 for x in my_list]
Wall time: 753 ms

正如書中所說,基於NumPy的演算法要比純Python快了接近50倍

NumPy的ndarray:一種多維陣列物件

建立ndarray

建立陣列最簡單的辦法就是使用array函式。它接受一切序列型的物件(包括其他陣列),然後產生一個新的含有傳入資料的NumPy陣列。以一個列表的轉換為例:

In [19]: data1 = [6, 7.5, 8, 0, 1]

In [20]: arr1 = np.array(data1)

In [21]: arr1
Out[21]: array([ 6. ,  7.5,  8. ,  0. ,  1. ])

In [25]: arr2.ndim
Out[25]: 2

In [26]: arr2.shape
Out[26]: (2, 4)

注:這裡的ndim和shape是陣列的維度屬性
可以看出,array函式中所有元素型別必須相同,不相同的會被強制轉換,同時他還會為所建的陣列中資料推斷出一個合適的資料型別,除非有特殊說明

In [11]: data1 = [6, 7.5, 8, 0, 1]

In [12]: arr1 = np.array(data1)

In [13]: arr1
Out[13]: array([6. , 7.5, 8. , 0. , 1. ])

In [14]: data2 = [[1, 2, 3, 4], [5, 6, 7, 8]]

In [15]: arr2 = np.array(data2)

In [16]: arr2
Out[16]:
array([[1, 2, 3, 4],
       [5, 6, 7, 8]])

In [17]: arr1.dtype
Out[17]: dtype('float64')//陣列一是浮點型所以自動分配了浮點型

In [18]: arr2.dtype
Out[18]: dtype('int32')//陣列二是整型所以自動分配了浮點型

此外,zeros和ones分別可以建立指定長度或形狀的全0或全1陣列,empty可以建立一個沒有任何具體值的陣列,要用這些方法建立多維陣列,只需傳入一個表示形狀的元組即可

In [29]: np.zeros(10)
Out[29]: array([ 0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.,  0.])

In [30]: np.zeros((3, 6))
Out[30]: 
array([[ 0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.,  0.],
       [ 0.,  0.,  0.,  0.,  0.,  0.]])

In [31]: np.empty((2, 3, 2))
Out[31]: 
array([[[ 0.,  0.],
        [ 0.,  0.],
        [ 0.,  0.]],
       [[ 0.,  0.],
        [ 0.,  0.],
        [ 0.,  0.]]])

而arange是Python內建函式range的陣列版
由於NumPy關注的是數值計算,因此,如果沒有特別指定,資料型別基本都是float64(浮點數)

NumPy陣列的運算

陣列很重要,因為它使你不用編寫迴圈即可對資料執行批量運算。NumPy使用者稱其為向量化(vectorization)。大小相等的陣列之間的任何算術運算都會將運算應用到元素級
陣列與標量的算術運算會將標量值傳播到各個元素
大小相同的陣列之間的比較會生成布林值陣列

基本的索引和切片

當你將一個標量值賦值給一個切片時(如arr[5:8]=12),該值會自動傳播(也就說後面將會講到的“廣播”)到整個選區。跟列表最重要的區別在於,陣列切片是原始陣列的檢視。這意味著資料不會被複制,檢視上的任何修改都會直接反映到源陣列上

In [60]: arr = np.arange(10)
In [61]: arr
Out[61]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

In [64]: arr[5:8] = 12
In [65]: arr
Out[65]: array([ 0,  1,  2,  3,  4, 12, 12, 12,  8,  9])

由於numpy處理資料時並不會隨意複製資料,所以效能和優化都得以大大的提升
同時在訪問多維陣列的時候,可以傳入一個以逗號隔開的索引列表來選取單個元素,也可以用傳統的C中陣列讀取方式,這兩者等價

切片索引

對於二維陣列其切片方式稍顯不同,它是沿著第0軸(即第一個軸)切片的。也就是說,切片是沿著一個軸向選取元素的。表示式arr2d[:2]可以被認為是“選取arr2d的前兩行”。

花式索引

花式索引(Fancy indexing)是一個NumPy術語,它指的是利用整數陣列進行索引。假設我們有一個8×4陣列

In [117]: arr = np.empty((8, 4))

In [118]: for i in range(8):
   .....:     arr[i] = i

In [119]: arr
Out[119]: 
array([[ 0.,  0.,  0.,  0.],
       [ 1.,  1.,  1.,  1.],
       [ 2.,  2.,  2.,  2.],
       [ 3.,  3.,  3.,  3.],
       [ 4.,  4.,  4.,  4.],
       [ 5.,  5.,  5.,  5.],
       [ 6.,  6.,  6.,  6.],
       [ 7.,  7.,  7.,  7.]])

為了以特定順序選取行子集,只需傳入一個用於指定順序的整數列表或ndarray即可:

In [120]: arr[[4, 3, 0, 6]]
Out[120]: 
array([[ 4.,  4.,  4.,  4.],
       [ 3.,  3.,  3.,  3.],
       [ 0.,  0.,  0.,  0.],
       [ 6.,  6.,  6.,  6.]])

陣列轉置和軸對換

轉置是重塑的一種特殊形式,它返回的是源資料的檢視(不會進行任何複製操作)。陣列不僅有transpose方法,還有一個特殊的T屬性:

In [126]: arr = np.arange(15).reshape((3, 5))

In [127]: arr
Out[127]: 
array([[ 0,  1,  2,  3,  4],
       [ 5,  6,  7,  8,  9],
       [10, 11, 12, 13, 14]])

In [128]: arr.T
Out[128]: 
array([[ 0,  5, 10],
       [ 1,  6, 11],
       [ 2,  7, 12],
       [ 3,  8, 13],
       [ 4,  9, 14]])

通用函式:快速的元素級陣列函式

(總算看到這裡)




利用陣列進行資料處理

該函式的求值運算就好辦了,把這兩個陣列當做兩個浮點數那樣編寫表示式即可
其中meshgrid函式將兩個輸入的陣列x和y進行擴充套件,前一個的擴充套件與後一個有關,後一個的擴充套件與前一個有關,前一個是豎向擴充套件,後一個是橫向擴充套件。因為,y的大小為2,所以x豎向擴充套件為原來的兩倍,而x的大小為3,所以y橫向擴充套件為原來的3倍。通過meshgrid函式之後,輸入由原來的陣列變成了一個矩陣

將條件邏輯表述為陣列運算

numpy.where函式是三元表示式x if condition else y的向量化版本
解釋:比如我有三個陣列,陣列一陣列二,外加一個布林型別陣列,我希望如果布林陣列是ture就選陣列一否則選陣列二,如果用純python寫,則是

x if c else y

這樣處理大資料時會及慢,若使用np.where,則只需要

np.where(c, x, y)

即可

np.where的第二個和第三個引數不必是陣列,它們都可以是標量值。在資料分析工作中,where通常用於根據另一個數組而產生一個新的陣列。假設有一個由隨機資料組成的矩陣,你希望將所有正值替換為2,將所有負值替換為-2。若利用np.where,則會非常簡單:

In [172]: arr = np.random.randn(4, 4)

In [173]: arr
Out[173]: 
array([[-0.5031, -0.6223, -0.9212, -0.7262],
       [ 0.2229,  0.0513, -1.1577,  0.8167],
       [ 0.4336,  1.0107,  1.8249, -0.9975],
       [ 0.8506, -0.1316,  0.9124,  0.1882]])

In [174]: arr > 0
Out[174]: 
array([[False, False, False, False],
       [ True,  True, False,  True],
       [ True,  True,  True, False],
       [ True, False,  True,  True]], dtype=bool)

In [175]: np.where(arr > 0, 2, -2)
Out[175]: 
array([[-2, -2, -2, -2],
       [ 2,  2, -2,  2],
       [ 2,  2,  2, -2],
       [ 2, -2,  2,  2]])

數學和統計方法

可以通過陣列上的一組數學函式對整個陣列或某個軸向的資料進行統計計算。sum、mean以及標準差std等聚合計算(aggregation,通常叫做約簡(reduction))既可以當做陣列的例項方法呼叫,也可以當做頂級NumPy函式使用。
(這裡mean是計算平均數)

排序

跟Python內建的列表型別一樣,NumPy陣列也可以通過sort方法就地排序

唯一化以及其它的集合邏輯

NumPy提供了一些針對一維ndarray的基本集合運算。最常用的可能要數np.unique了,它用於找出陣列中的唯一值並返回已排序的結果

線性代數

偽隨機數生成

numpy.random模組對Python內建的random進行了補充,增加了一些用於高效生成多種概率分佈的樣本值的函式。

可以用normal來得到一個標準正態分佈的4×4樣本陣列

In [238]: samples = np.random.normal(size=(4, 4))

In [239]: samples
Out[239]: 
array([[ 0.5732,  0.1933,  0.4429,  1.2796],
       [ 0.575 ,  0.4339, -0.7658, -1.237 ],
       [-0.5367,  1.8545, -0.92  , -0.1082],
       [ 0.1525,  0.9435, -1.0953, -0.144 ]])

我們說這些都是偽隨機數,是因為它們都是通過演算法基於隨機數生成器種子,在確定性的條件下生成的。你可以用NumPy的np.random.seed更改隨機數生成種子
numpy.random的資料生成函式使用了全域性的隨機種子。要避免全域性狀態,你可以使用numpy.random.RandomState,建立一個與其它隔離的隨機數生成器:

In [245]: rng = np.random.RandomState(1234)

In [246]: rng.randn(10)
Out[246]: 
array([ 0.4714, -1.191 ,  1.4327, -0.3127, -0.7206,  0.8872,  0.8596,
       -0.6365,  0.0157, -2.2427])


示例:隨機漫步

書中使用純python實現1000步的隨機漫步(從0開始,步長1和-1出現的概率相等)

In [247]: import random
   .....: position = 0
   .....: walk = [position]
   .....: steps = 1000
   .....: for i in range(steps):
   .....:     step = 1 if random.randint(0, 1) else -1   
   //對於random.randint,作用是引數1、引數2必須是整數,函式返回引數1和引數2之間的任意整數, 閉區間
   .....:     position += step
   .....:     walk.append(position)
   .....:

這裡我的理解是random.randint在0,1之間挑一個,如果是1就輸出1,是0就輸出-1
然後如果用numpy裡面的random,程式碼如下

In [251]: nsteps = 1000

In [252]: draws = np.random.randint(0, 2, size=nsteps)

In [253]: steps = np.where(draws > 0, 1, -1)

In [254]: walk = steps.cumsum()

這裡一開始我很不理解,np.random.randint(0, 2, size=nsteps)中我以為是0,1,2選擇,結果我嘗試輸出

In [53]: nsteps = 1000

In [54]: draws = np.random.randint(0, 2, size=nsteps)

In [55]: draws
Out[55]:
array([0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1,
       0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1,
       0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 0,
       0, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
       1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1,
       1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1,
       1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0,
       0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0,
       1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0,
       1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1,
       1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1,
       1, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1,
       1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1,
       0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0,
       0, 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0,
       0, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0,
       0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0,
       1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1,
       1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1,
       0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1,
       1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 0, 1,
       0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1,
       1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0,
       0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0,
       0, 1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1,
       0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0,
       1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0,
       1, 0, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1,
       0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0,
       1, 0, 1, 1, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1, 1,
       0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0,
       0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0,
       1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1,
       1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0, 0,
       1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1,
       0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 0, 1, 1,
       0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0,
       1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1,
       0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1,
       1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 0,
       0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1,
       1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0,
       0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 1,
       1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0,
       0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 0, 0, 1,
       0, 0, 0, 0, 0, 1, 1, 1, 1, 1])

In [56]:

發現其實np.random.randint(0, 2, size=nsteps)只產生了0,1
經過查閱才知道np.random.randint是返回一個隨機整型數,範圍從低(包括)到高(不包括),即[low, high)。而python裡面的random是直接閉區間,上下都有包含

一次模擬多個隨機漫步

如果希望模擬多個隨機漫步過程(比如5000個),只需對上面的程式碼做一點點修改即可生成所有的隨機漫步過程。只要給numpy.random的函式傳入一個二元元組就可以產生一個二維陣列,然後我們就可以一次性計算5000個隨機漫步過程(一行一個)的累計和了:

In [258]: nwalks = 5000

In [259]: nsteps = 1000

In [260]: draws = np.random.randint(0, 2, size=(nwalks, nsteps)) # 0 or 1

In [261]: steps = np.where(draws > 0, 1, -1)

In [262]: walks = steps.cumsum(1)

In [263]: walks
Out[263]: 
array([[  1,   0,   1, ...,   8,   7,   8],
       [  1,   0,  -1, ...,  34,  33,  32],
       [  1,   0,  -1, ...,   4,   5,   4],
       ..., 
       [  1,   2,   1, ...,  24,  25,  26],
       [  1,   2,   3, ...,  14,  13,  14],
       [ -1,  -2,  -3, ..., -24, -23, -22]])

小結

NumPy基礎:陣列和向量計算基本結束,爭取晚上把第五章拿下!