參考書籍 《利用Python進行數據分析(原書第2版)》第4章 NumPy基礎:數組與向量化計算
NumPy => Numerical Python的簡稱
@多數情況下,數據分析應用關注的內容
· 在數據處理、清洗、構造子集、過濾、變換以及其他計算中進行快速的向量化計算。
· 常見的數組算法,比sort、unique以及set操作等。
· 高效的描述性統計和聚合/概述數據。
· 數據排列和相關數據操作,例如對異構數據進行merge和join。
· 使用數組表達式來表明條件邏輯,代替if-elif-else條件分支的循環。
· 分組數據的操作(聚合、變換以及函數式操作)。
@NumPy涉及的內容
· ndarray,一種高效多維數組
· 對所有數據進行快速的矩陣計算 => 無須編寫循環程序 [1]
· 對硬盤中數組數據進行讀寫的工具,並對內存映射文件進行操作。
· 線性代數、隨機數生成以及傅里葉變換功能。
· 用於連接NumPy到C、C++和FORTRAN語言類庫的C語言API。
[1] NumPy可以針對全量數組進行復雜計算而不需要寫Python循環。
【例】假設一個NumPy數組包含10萬個整數,一個Python列表包含同樣內容
操作:同時對每個序列乘以2,看看花費的時間
In [1]: import numpy as np
In [2]: my_arr = np.arange(100000)
In [3]: my_list = list(range(100000))
In [4]: %time for i in range(10): my_arr2 = my_arr * 2
Wall time: 1.99 ms
In [5]: %time for i in range(10): my_list2 = [x * 2 for x in my_list]
Wall time: 126 ms
In [6]:
可以看出,NumPy的方法比Python方法要快,並且使用的內存也更少。
說明:系統時間(Wall clock time) 指一段程序從運行到終止,系統時鐘走過的時間。
CPU time和Wall time 參考閱讀 -> https://blog.csdn.net/filyouzicha/article/details/52447887
進程時間也稱CPU時間,用以度量進程使用的中央處理器資源。CPU總時間(user + sys)= 用戶時間(user)和系統時間(sys)。
實際時間指實際流逝的時間,是從進行開始執行到完成所經歷的牆上時鐘時間(wall clock)時間。
注意:Python列表直接乘以2的含義是list + list,即將兩個列表拼在一起。
In [7]: my_arr = np.arange(10)
In [8]: my_arr
Out[8]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [9]: my_list = list(range(10))
In [10]: my_list
Out[10]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
In [11]: my_arr * 2
Out[11]: array([ 0, 2, 4, 6, 8, 10, 12, 14, 16, 18])
In [12]: my_list * 2
Out[12]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
In [13]: my_list2 = list(range(10))
In [15]: [x * 2 for x in my_list2]
Out[15]: [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
In [16]:
約定:以下所說的數組都是指ndarray對象
4.1 NumPy ndarray:多維數組對象
NumPy的核心特徵之一就是N-維數組對象——ndarray。
@數組的三個屬性:維度ndim,每一維度的數量shape,數據類型ntype
一個ndarray是一個多維同類數據容器,同類數據即它包含的每一個元素均爲相同類型。每一個數組都有一個ndim屬性,用來描述數組的維度;每一個數組都有一個shape屬性,用來表徵數組每一維度的數量;每一個數組都有一個dtype屬性,用來描述數組的數據類型。
數組之間的加減乘除是針對每一個位置的標量兩兩對應操作,即允許你進行批量操作而無須任何for循環。NumPy用戶稱這種特性爲向量化。任何在兩個等尺寸數組之間的算術操作都應用了逐元素操作的方式。
【例】生成一個2x3 隨機數矩陣,進行加法和乘法操作。查看shape屬性值和dtype屬性值。
查看ndim屬性值。
同尺寸數組之間的比較,會產生一個布爾值數組;不同尺寸的數組間的操作,將會用到廣播特性(暫做了解)。
【例】兩個2x2數組比較
In [16]: arr1 = np.array([[1,2],[3,4]])
In [17]: arr2 = np.array([[4,3],[2,1]])
In [18]: arr1 > arr2
Out[18]:
array([[False, False],
[ True, True]])
In [19]:
@生成數組的方法
【例】以二維矩陣 2x3 爲例
1)生成隨機數矩陣 np.random.randn(2,3)
2)np.array() 接收二維列表two_d_list => np.array(two_d_list)
3)生成全0數組 np.zeros((2,3))
4)生成全1數組 np.ones((2,3))
@arange是Python內建函數range的數組版
In [19]: np.arange(15)
Out[19]: array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
@選擇單個元素的兩種方法
【例】以二維數組爲例,選擇第1行第2列,可以使用 arr[0][1] 或 arr[0,1]
In [53]: arr = np.array([[11,12,13],[21,22,23]])
In [54]: arr
Out[54]:
array([[11, 12, 13],
[21, 22, 23]])
In [55]: arr[0][1]
Out[55]: 12
In [56]: arr[0,1]
Out[56]: 12
@切片
如果你傳入了一個數值給數組的切片,例如arr1[5:8] = 12,數值被傳遞給了整個切片。
注意:區別於Python的內建列表,數組的切片是原數組的視圖。這意味着數據並不是被複制了,任何對於視圖的修改都會反映到原數組上。
【例】改變數組切片中的元素,原數組也會變化
In [38]: list1 = list(range(10))
In [39]: arr1 = np.arange(10)
In [40]: list1_slice = list1[5:8]
In [41]: arr1_slice = arr1[5:8]
In [42]: list1_slice[0] = 666
In [43]: arr1_slice[0] = 666
In [44]: list1_slice
Out[44]: [666, 6, 7]
In [45]: arr1_slice
Out[45]: array([666, 6, 7])
In [46]: list1
Out[46]: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
In [47]: arr1
Out[47]: array([ 0, 1, 2, 3, 4, 666, 6, 7, 8, 9])
如果你想要一份數組切片的拷貝而不是一份視圖的話,必須顯式地複製這個數組,例如 arr1[5:8].copy()
In [48]: arr1 = np.arange(10)
In [49]: arr1_slice = arr1[5:8].copy()
In [50]: arr1_slice[0] = 666
In [51]: arr1_slice
Out[51]: array([666, 6, 7])
In [52]: arr1
Out[52]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
@數組的切片索引 => 按照維度依次切片
我覺得這樣比較好理解~ 想象有一個方形蛋糕
二維數組 => 俯視蛋糕,有橫縱兩種刀法切蛋糕
三位數組 => 在上述基礎上,增加一種刀法——刀面平行於桌子,從蛋糕側面橫切
【例】3x3二維數組切片(以下描述從0開始計數)
arr2d[ :2,1:] 先切第一個維度 :2 => 選擇第0~1行;再切第二個維度 1: => 選擇第1~2列
arr2d[2] 等價於 arr2d[2,: ],即只切了第一個維度 => 選擇第2行
@數組的轉置換和換軸
我對轉置矩陣的形象理解~ 把二維矩陣想象成一個矩形,沿着左上右下頂點連接的對角線翻折就得到轉置矩陣啦~其實就是x軸變成y軸,y軸變成x軸。
轉置可以理解爲換軸的特殊情況。數組擁有transpose方法,也有特殊的T屬性。
【例】創建一個3x5 矩陣,首先使用T屬性轉置
In [57]: arr = np.arange(15).reshape((3, 5))
In [58]: arr
Out[58]:
array([[ 0, 1, 2, 3, 4],
[ 5, 6, 7, 8, 9],
[10, 11, 12, 13, 14]])
In [59]: arr.T
Out[59]:
array([[ 0, 5, 10],
[ 1, 6, 11],
[ 2, 7, 12],
[ 3, 8, 13],
[ 4, 9, 14]])
使用transpose方法可以達到相同效果,如下第0個參數的值爲1 => 0軸換成1軸;第1個參數的值爲0 => 1軸換成0軸
In [63]: arr.transpose((1,0))
Out[63]:
array([[ 0, 5, 10],
[ 1, 6, 11],
[ 2, 7, 12],
[ 3, 8, 13],
[ 4, 9, 14]])
應用:使用np.dot計算內積
In [64]: np.dot(arr.T,arr)
Out[64]:
array([[125, 140, 155, 170, 185],
[140, 158, 176, 194, 212],
[155, 176, 197, 218, 239],
[170, 194, 218, 242, 266],
[185, 212, 239, 266, 293]])
In [65]: np.dot(arr,arr.T)
Out[65]:
array([[ 30, 80, 130],
[ 80, 255, 430],
[130, 430, 730]])
對於更高維度的數組,transpose方法可以接收包含軸編號的元組,用於置換軸。
【例】三維數組置換軸
In [66]: arr = np.arange(16).reshape((2, 2, 4))
In [67]: arr
Out[67]:
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7]],
[[ 8, 9, 10, 11],
[12, 13, 14, 15]]])
In [68]: arr.transpose((1, 0, 2))
Out[68]:
array([[[ 0, 1, 2, 3],
[ 8, 9, 10, 11]],
[[ 4, 5, 6, 7],
[12, 13, 14, 15]]])
In [70]: arr.shape
Out[70]: (2, 2, 4)
In [71]: arr.transpose((1, 0, 2)).shape
Out[71]: (2, 2, 4)
arr.transpose((1, 0, 2)) => 原0軸變爲1軸,原1軸變成0軸,最後的2軸沒有改變。
4.2 通用函數:快速的逐元素數組函數
通用函數,也可以稱爲ufunc,是一種在ndarray數據中進行逐元素操作的函數。
有很多ufunc是簡單的逐元素轉換,比如square, sqrt或exp函數。接收一個數組 => 一元通用函數
In [78]: arr = np.arange(8)
In [79]: arr1 = np.square(arr)
In [80]: arr1
Out[80]: array([ 0, 1, 4, 9, 16, 25, 36, 49], dtype=int32)
In [81]: arr2 = np.sqrt(arr1)
In [82]: arr2
Out[82]: array([0., 1., 2., 3., 4., 5., 6., 7.])
In [86]: arr2_2 = np.array(np.sqrt(arr1),dtype=np.int32)
In [87]: arr2_2
Out[87]: array([0, 1, 2, 3, 4, 5, 6, 7])
還有一些通用函數,比如add或maximum,接收兩個數組並返回一個數組作爲結果 => 二元通用函數
也有一些通用函數返回多個數組。比如modf,它返回一個浮點值數組的小數部分和整數部分。
In [88]: arr = np.random.randn(2,3)
In [89]: arr
Out[89]:
array([[-0.91043845, 0.44003487, 1.26963933],
[-0.08591442, -0.40683512, 1.11877103]])
In [90]: remainder, whole_part = np.modf(arr)
In [91]: remainder
Out[91]:
array([[-0.91043845, 0.44003487, 0.26963933],
[-0.08591442, -0.40683512, 0.11877103]])
In [92]: whole_part
Out[92]:
array([[-0., 0., 1.],
[-0., -0., 1.]])
4.3 使用數組進行面向數組編程
numpy.where函數是三元表達式x if condition else y的向量化版本。
假設我們有一個布爾值數組cond和兩個數值數組,假設cond中的元素爲True時,取xarr中的對應元素值,否則取yarr中的元素。
我們可以通過列表推導式來完成,像下列代碼這樣:
In [93]: xarr = np.array([1.1, 1.2, 1.3, 1.4, 1.5])
In [94]: yarr = np.array([2.1, 2.2, 2.3, 2.4, 2.5])
In [95]: cond = np.array([True, False, True, True, False])
In [96]: result = [(x if c else y)
...: for x, y, c in zip(xarr, yarr, cond)]
In [97]: result
Out[97]: [1.1, 2.2, 1.3, 1.4, 2.5]
這樣會產生的問題:
首先,如果數組很大的話,速度會很慢;
其次,當數組是多維時,就無法湊效了。而使用np.where時,就可以非常簡單地完成:
In [98]: result = np.where(cond, xarr, yarr)
In [99]: result
Out[99]: array([1.1, 2.2, 1.3, 1.4, 2.5])
傳遞給np.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]])
可以使用np.where將標量和數組聯合,例如,我可以使用 np.where(arr > 0, 2, arr) 將arr中的所有正值替換爲常數2。
4.3.2 數學和統計方法
使用聚合函數(通常也叫縮減函數),比如sum、mean和std(標準差),可以接收一個可選參數axis,這個參數可以用於計算給定軸向上的統計值,形成一個下降一維度的數組 => 沿着選定軸拍扁(可以用二維、三維矩陣腦補一下)
arr.mean(1)表示“計算每一列的平均值”
arr.sum(0)表示“計算行軸向的累和”
這裏理解了很久,特別是三維情況,於我畫了圖理解,如下
1)二維情況
【例】arr.mean(1)表示 以1軸求平均,這裏的1軸就是下圖的Y軸
2)三維情況
增加了一個維度,所以加上Z軸,特別注意,此時的0軸已經不是X軸了(這裏卡殼很久= =)。如下是一個2x2x2的三維數組,可以把arr[0]理解爲第一個面,注意元素的標識,會發現代表第幾個面的是標識的第一個參數(而非第三個)。
形象化如下圖:
四維可以想象不同時間有同大小立方體,沿着時間軸拍扁,更多維度就不糾結了,直接用純數學思維理解更簡單。
回到這一章節- -
其它的方法,例如cumsum和cumprod並不會聚合,它們會產生一箇中間結果 => 沿着選定軸逐級拍扁
在多維數組中,像cumsum這樣的累積函數返回相同長度的數組,但是可以在指定軸向上根據較低維度的切片進行部分聚合。
【例】sum,cumsum和cumprod
In [100]: arr = np.arange(6).reshape(2,3)
In [101]: arr
Out[101]:
array([[0, 1, 2],
[3, 4, 5]])
In [107]: arr[0]
Out[107]: array([0, 1, 2])
In [108]: arr.cumsum(axis=0)
Out[108]:
array([[0, 1, 2],
[3, 5, 7]], dtype=int32)
In [109]: arr.cumsum(axis=1)
Out[109]:
array([[ 0, 1, 3],
[ 3, 7, 12]], dtype=int32)
In [110]: arr.cumprod(axis=0)
Out[110]:
array([[ 0, 1, 2],
[ 0, 4, 10]], dtype=int32)
In [111]: arr.cumprod(axis=1)
Out[111]:
array([[ 0, 0, 0],
[ 3, 12, 60]], dtype=int32)
In [112]: arr.sum(axis=0)
Out[112]: array([3, 5, 7])
In [113]: arr.sum(axis=1)
Out[113]: array([ 3, 12])
4.3.3 布爾值數組的方法
布爾值會被強制爲1(True)和0(False)。因此,sum通常可以用於計算布爾值數組中的True的個數:
【例】統計隨機數矩陣中的正值個數
In [190]: arr = np.random.randn(100)
In [191]: (arr > 0).sum()
Out[191]: 42
對於布爾值數組,有兩個非常有用的方法any和all。
any檢查數組中是否至少有一個True
all檢查是否每個值都是True
In [192]: bools = np.array([False, False, True, False])
In [193]: bools.any()
Out[193]: True
In [194]: bools.all()
Out[194]: False
這些方法也可適用於非布爾值數組,所有的非0元素都會按True處理。
4.3.4 排序
和Python的內建列表類型相似,NumPy數組可以使用sort方法按位置排序:
In [195]: arr = np.random.randn(6)
In [196]: arr
Out[196]: array([ 0.6095, -0.4938, 1.24 , -0.1357, 1.43 , -0.8469])
In [197]: arr.sort()
In [198]: arr
Out[198]: array([-0.8469, -0.4938, -0.1357, 0.6095, 1.24 , 1.43 ])
還可以在多維數組中根據傳遞的axis值,沿着軸向對每個一維數據段進行排序:
In [199]: arr = np.random.randn(5, 3)
In [200]: arr
Out[200]:
array([[ 0.6033, 1.2636, -0.2555],
[-0.4457, 0.4684, -0.9616],
[-1.8245, 0.6254, 1.0229],
[ 1.1074, 0.0909, -0.3501],
[ 0.218 , -0.8948, -1.7415]])
In [201]: arr.sort(1)
In [202]: arr
Out[202]:
array([[-0.2555, 0.6033, 1.2636],
[-0.9616, -0.4457, 0.4684],
[-1.8245, 0.6254, 1.0229],
[-0.3501, 0.0909, 1.1074],
[-1.7415, -0.8948, 0.218 ]])
4.3.5 唯一值與其他集合邏輯
NumPy包含一些針對一維ndarray的基礎集合操作(可以與Python的set比較)。
@np.unique => 返回的是數組中唯一值排序後形成的數組
In [206]: names =np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
In [207]: np.unique(names)
Out[207]:
array(['Bob', 'Joe', 'Will'],
dtype='<U4')
In [208]: ints = np.array([3, 3, 3, 2, 2, 1, 1, 4, 4])
In [209]: np.unique(ints)
Out[209]: array([1, 2, 3, 4])
@np.in1d,可以檢查一個數組中的值是否在另外一個數組中,並返回一個布爾值數組
In [211]: values = np.array([6, 0, 0, 3, 2, 5, 6])
In [212]: np.in1d(values, [2, 3, 6])
Out[212]: array([ True, False, False, True, True, False, True], dtype=bool)
4.4 使用數組進行文件輸入和輸出
np.save和np.load是高效存取硬盤數據的兩大工具函數。
數組在默認情況下是以未壓縮的格式進行存儲的,後綴名是.npy(如果文件存放路徑中沒寫.npy,會自動加上)。
In [213]: arr = np.arange(10)
In [214]: np.save('some_array', arr)
硬盤上的數組可以使用np.load進行載入:
In [215]: np.load('some_array.npy')
Out[215]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
你可以使用np.savez並將數組作爲參數傳遞給該函數,用於在未壓縮文件中保存多個數組,
當載入一個.npz文件的時候,你會獲得一個字典型的對象,並通過該對象很方便地載入單個數組:
In [216]: np.savez('array_archive.npz', a=arr, b=arr*2)
In [217]: arch = np.load('array_archive.npz')
In [218]: arch['b']
Out[218]: array([ 0, 2, 4, 6, 8, 10, 12, 14, 16, 18])
4.5 線性代數
@點乘
NumPy中,*是矩陣的逐元素乘積;點乘可以使用函數dot或特殊符號@
x.dot(y) <=> np.dot(x, y) <=> x @ y
In [142]: x = np.arange(6).reshape(2,3)
In [143]: x
Out[143]:
array([[0, 1, 2],
[3, 4, 5]])
In [144]: y =np.arange(6).reshape(3,2)
In [145]: y
Out[145]:
array([[0, 1],
[2, 3],
[4, 5]])
In [146]: x.dot(y)
Out[146]:
array([[10, 13],
[28, 40]])
In [148]: np.dot(x,y)
Out[148]:
array([[10, 13],
[28, 40]])
In [149]: x@y
Out[149]:
array([[10, 13],
[28, 40]])
numpy.linalg擁有一個矩陣分解的標準函數集,以及其它常用函數,例如求逆和行列式求解。
4.6 僞隨機數生成
使用normal來獲得一個4×4的正態分佈樣本數組=> np.random.normal(size=(4, 4))
我們可以生成的數據爲僞隨機數,因爲它們是由具有確定性行爲的算法根據隨機數生成器中的隨機數種子生成的。
4.7 示例:隨機漫步
首先,讓我們考慮一個簡單的隨機漫步,從0開始,步進爲1和-1,且兩種步進發生的概率相等。
以下是使用內建random模塊利用純Python實現的一個1000步的隨機漫步:
In [247]: 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)
.....:
上述walk只是對隨機步進的累積,並且可以通過一個數組表達式實現。
現在使用np.random模塊一次性抽取1000次投擲硬幣的結果,每次投擲的結果爲1或-1,然後計算累積值:
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()
說明:numpy.random.randint(low, high=None, size=None, dtype='l')
函數返回一個隨機整型數組,如果提供了high,每個數在範圍[low, high);否則範圍是[0,low)。
size可以是一個整數(一維數組)或者元組(多維數組)。
由此我們開始從漫步軌道上提取一些統計數據,比如最大值、最小值等:
In [255]: walk.min()
Out[255]: -3
In [256]: walk.max()
Out[256]: 31
@一次性模擬多次隨機漫步
如果你的目標是模擬多次隨機漫步,比如說5000次,每次1000步。
=> size傳入一個2個元素的元組,numpy.random中的函數可以生成一個二維的抽取數組,並且可以使用cumsum一次性地跨行算出全部5000次隨機步的累積和:
In [1]: nwalks = 5000 #進行5000次隨機漫步
In [2]: nsteps = 1000 #每次隨機漫步走1000步
In [5]: draws = np.random.randint(0, 2, size=(nwalks, nsteps)) #生成5000x1000矩陣,隨機值爲0或1
In [6]: steps = np.where(draws > 0, 1, -1) #上述矩陣值爲1的位置返回1,否則返回-1,從而得到一個5000x1000矩陣,元素值值爲1或-1
In [7]: walks = steps.cumsum(1) #每一行都是一次漫步,在行的方向上(沿着1軸)進行逐個累加,得到一個5000x1000矩陣
In [8]: walks
Out[8]:
array([[ -1, 0, 1, ..., -6, -7, -6],
[ 1, 2, 3, ..., 18, 19, 18],
[ 1, 0, 1, ..., -8, -9, -8],
...,
[ -1, 0, 1, ..., 38, 37, 36],
[ 1, 2, 1, ..., -30, -29, -30],
[ 1, 2, 3, ..., 26, 27, 26]], dtype=int32)
In [9]: walks.max()
Out[9]: 140
In [10]: walks.min()
Out[10]: -107