本博客爲《利用Python進行數據分析》的讀書筆記,請勿轉載用於其他商業用途。
文章目錄
NumPy是Numerical Python的簡稱。
1. NumPy ndarray:多維數組對象
Numpy的核心特徵之一就是N-維數組對象——ndarray。ndarray是Python中一個快速、靈活的大型數據集容器。數組允許你使用類似於標量的操作語法在整塊數據上進行科學計算。
爲了感受下Numpy如何使用類似於內建對象的標量計算語法進行批量計算,我們首先導入Numpy,再生成一個小的隨機數組:
data = np.random.randn(2, 3)
data
#
array([[-0.26900244, -0.66725718, 0.73856274],
[ 0.79269933, -1.56455072, 1.64151292]])
然後再給data加上一個數學操作:
data * 10
#
array([[ -2.69002443, -6.6725718 , 7.38562744],
[ 7.92699329, -15.6455072 , 16.41512923]])
data + data
#
array([[-0.53800489, -1.33451436, 1.47712549],
[ 1.58539866, -3.12910144, 3.28302585]])
一個ndarray是一個通用的多維同類數據容器,也就是說,它包含的每一個元素均爲相同類型。每一個數組都有一個shape
屬性,用來表徵數組每一維度的數量;每一個數組都有一個dtype
屬性,用來描述數組的數據類型:
data.shape
#
(2, 3)
data.dtype
#
dtype('float64')
1.1 生成ndarray
生成數組最簡單的方式就是使用array函數。array函數接收任意的序列型對象,生成一個新的包含傳遞數組的Numpy數組。例如,列表的轉換就是一個好例子:
data1 = [6, 7.5, 8, 0, 1]
arr1 = np.array(data1)
arr1
#
array([6. , 7.5, 8. , 0. , 1. ])
嵌套序列,例如同等長度的列表,將會自動轉換成多維數組:
data2 = [[1, 2, 3, 4], [5, 6, 7, 8]]
arr2 = np.array(data2)
arr2
#
array([[1, 2, 3, 4],
[5, 6, 7, 8]])
因爲data2是一個包含列表的列表,所以Numpy數組arr2形成了二維數組。我們可以通過檢查ndim
和shape
屬性來確認這一點:
arr2.ndim
#
2
arr2.shape
#
(2, 4)
除非顯式地指定,否則np.array
會自動推斷生成數組的數據類型。數據類型被存儲在一個特殊的元數據dtype
中。例如,之前的兩個例子:
arr1.dtype
#
dtype('float64')
arr2.dtype
#
dtype('int32')
除了np.array
,還有很多其他函數可以創建新函數。例如,給定長度及形狀後,zeros
可以一次性創造全0數組,ones
可以一次性創造全1數組。empty
可以創建一個沒有初始化數值的數組。想要創建高維數組,則需要爲shape
傳遞一個元組:
np.zeros(10)
#
array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
np.zeros((3, 6))
#
array([[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.],
[0., 0., 0., 0., 0., 0.]])
np.empty((2, 3, 2))
#
array([[[5.67027464e-312, 2.47032823e-322],
[0.00000000e+000, 0.00000000e+000],
[0.00000000e+000, 8.60952352e-072]],
[[5.33502485e-091, 4.08370620e-033],
[2.82953568e-033, 3.34488015e-061],
[3.99910963e+252, 4.93432906e+257]]])
注意:想要用np.empty
來生成一個全0數組並不安全,有些時候它可能會返回未初始化的垃圾數值。如上述代碼塊。
arange
是Python內建函數range
的數組版:
np.arange(15)
#
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])
由於Numpy專注於數值計算,如果沒有特別指明的話,默認的數據類型是float64
(浮點型)。
表:數組生成函數
函數名 | 描述 |
---|---|
array | 將輸入數據(可以是列表、元組、數組以及其他序列)轉換爲ndarray,如不顯式指明數據類型,將自動推斷;默認所有的輸入數據 |
asarray | 將輸入轉換爲ndarray,但如果輸入已經是ndarray則不再賦值 |
arange | Python內建函數range 的數組版,返回一個數組 |
ones | 根據給定形狀和數據類型生成全1數組 |
ones_like | 根據所給的數組生成一個形狀一樣的全1數組 |
zeros | 根據給定形狀和數據類型生成全1數組 |
zeros_like | 根據所給的數組生成一個形狀一樣的全0數組 |
empty | 根據給定形狀生成一個沒有初始化數值的空數組 |
emtpy_like | 根據所給數組生成一個形狀一樣但是沒有初始化數值的空數組 |
full | 根據給定的形狀和數據類型生成指定數值的數組 |
full_like | 根據所給的數組生成一個形狀一樣但是內容都是指定數值的數組 |
eye, identity | 生成一個N*N特徵矩陣(對角線位置都是1,其餘位置是0) |
1.2 ndarray的數據類型
dtype
(數據類型)是一個特殊的對象,它含有 ndarray
將一塊內存解釋爲特定數據類型所需的信息:
In [33]: arr1 = np.array([1, 2, 3], dtype=np.float64)
In [34]: arr2 = np.array([1, 2, 3], dtype=np.int32)
In [35]: arr1.dtype
Out[35]: dtype('float64')
In [36]: arr2.dtype
Out[36]: dtype('int32')
dtype 是 NumPy能夠與其他系統數據靈活交互的原因。通常,其他系統提供一個硬盤或內存與數據的對應關係,
使得利用 C或Fortran等底層語言讀寫數據變得十分方便。數值型 dtype 的命名方式相同:一個類型名(如 float 或 int),後面跟一個用於表示各元素位長的數字。標準的雙精度浮點值(即 Python 中的 float 對象)需要佔用 8 字節(即 64 位)。因此,該類型在 NumPy 中就記作 float64。表 4-2 列出了 NumPy 所支持的全部數據類型。
記不住這些 NumPy 的 dtype 也沒關係,新手更是如此。通常只需要知道你所處理的數據的大致類型是浮點數、複數、整數、布爾值、字符串,還是普通的 Python 對象即可。當你需要控制數據在內存和磁盤中的存儲方式時(尤其是對大數據集),那就得了解如何控制存儲類型。
你可以使用astype
方法顯示地轉換數組的數據類型:
In [37]: arr = np.array([1, 2, 3, 4, 5])
In [38]: arr.dtype
Out[38]: dtype('int64')
In [39]: float_arr = arr.astype(np.float64)
In [40]: float_arr.dtype
Out[40]: dtype('float64')
在上面例子中,整數被轉換成了浮點數。如果將浮點數轉換成整數,則小數部分將會被截取刪除:
In [41]: arr = np.array([3.7, -1.2, -2.6, 0.5, 12.9, 10.1])
In [42]: arr
Out[42]: array([ 3.7, -1.2, -2.6, 0.5, 12.9, 10.1])
In [43]: arr.astype(np.int32)
Out[43]: array([ 3, -1, -2, 0, 12, 10], dtype=int32)
注意:這裏是直接將小數點後的部分刪除,而不是四捨五入。
如果某字符串數組表示的全是數字,也可以用astype
將其轉換爲數值形式:
In [44]: numeric_strings = np.array(['1.25', '-9.6', '42'], dtype=np.string_)
In [45]: numeric_strings.astype(float)
Out[45]: array([ 1.25, -9.6 , 42. ])
注意,在Numpy中,當使用numpy.string_
類型作字符串數據要小心,因爲NumPy會修正它的大小或刪除輸入且不發出警告。pandas在處理非數值數據時有更直觀的開箱型操作。
你也可以使用另一個數組的的type屬性:
In [46]: int_array = np.arange(10)
In [47]: calibers = np.array([.22, .270, .357, .380, .44, .50], dtype=np.float64)
In [48]: int_array.astype(calibers.dtype)
Out[48]: array([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])
你還可以使用類型代碼來傳入數據類型:
In [49]: empty_uint32 = np.empty(8, dtype='u4')
In [50]: empty_uint32
Out[50]:
array([ 0, 1075314688, 0, 1075707904, 0,
1075838976, 0, 1072693248], dtype=uint32)
使用
astype
時總是生成一個新的數組,即時你傳入的dtype與之前一樣。
1.3 NumPy數組算術
數組是所以重要是因爲它允許你進行批量操作而無需任何for循環。NumPy用戶稱這種特性爲向量化。任何在兩個等尺寸數組之間的算數操作都應用了逐元素操作的方式:
In [51]: arr = np.array([[1., 2., 3.], [4., 5., 6.]])
In [52]: arr
Out[52]:
array([[ 1., 2., 3.],
[ 4., 5., 6.]])
In [53]: arr * arr
Out[53]:
array([[ 1., 4., 9.],
[ 16., 25., 36.]])
In [54]: arr - arr
Out[54]:
array([[ 0., 0., 0.],
[ 0., 0., 0.]])
帶有標量計算的算術操作,會把計算參數傳遞給數組的每一個元素:
In [55]: 1 / arr
Out[55]:
array([[ 1. , 0.5 , 0.3333],
[ 0.25 , 0.2 , 0.1667]])
In [56]: arr ** 0.5
Out[56]:
array([[ 1. , 1.4142, 1.7321],
[ 2. , 2.2361, 2.4495]])
同尺寸數組之間的比較,會產生一個布爾值數組:
In [57]: arr2 = np.array([[0., 4., 1.], [7., 2., 12.]])
In [58]: arr2
Out[58]:
array([[ 0., 4., 1.],
[ 7., 2., 12.]])
In [59]: arr2 > arr
Out[59]:
array([[False, True, False],
[ True, False, True]], dtype=bool)
不同尺寸的數組間的操作,將會用到廣播特性。
1.4 基礎索引與切片
一位數組比較簡單,看起來的和Python的列表很類似:
In [60]: arr = np.arange(10)
In [61]: arr
Out[61]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [62]: arr[5]
Out[62]: 5
In [63]: arr[5:8]
Out[63]: array([5, 6, 7])
In [64]: arr[5:8] = 12
In [65]: arr
Out[65]: array([ 0, 1, 2, 3, 4, 12, 12, 12, 8, 9])
如上所示,當你將一個標量值賦值給一個切片時(如 arr[5:8]=12),該值會自動傳播(也就說後面將會講到的 “廣播”)到整個選區。跟列表最重要的區別在於,數組切片是原始數組的視圖。這意味着數據不會被複制,視圖上的任何修改都會直接反映到源數組上。
作爲例子,先創建一個 arr 的切片:
In [66]: arr_slice = arr[5:8]
In [67]: arr_slice
Out[67]: array([12, 12, 12])
現在,當我修改 arr_slice 中的值,變動也會體現在原始數組上:
In [68]: arr_slice[1] = 12345
In [69]: arr
Out[69]: array([ 0, 1, 2, 3, 4, 12, 12345, 12, 8,
9])
不寫切片值的[:]將會引用數組的所有值:
In [70]: arr_slice[:] = 64
In [71]: arr
Out[71]: array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])
如果你剛開始接觸 NumPy,可能會對此感到驚訝(尤其是當你曾經用過其他熱衷於複製數組數據的編程語言)。由於 NumPy 的設計目的是處理大數據,所以你可以想象一下,假如 NumPy 堅持要將數據複製來複制去的話會產生何等的性能和內存問題。
如果你想要得到的是 ndarray 切片的一份副本而非視圖,就需要明確地進行復制操作,例如arr[5:8].copy()。
對於高維度數組,能做的事情更多。在一個二維數組中,各索引位置上的元素不再是標量而是一維數組:
In [72]: arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
In [73]: arr2d[2]
Out[73]: array([7, 8, 9])
因此,可以對各個元素進行遞歸訪問,但這樣需要做的事情有點多。你可以傳入一個以逗號隔開的索引列表來選取單個元素。也就是說,下面兩種方式是等價的:
In [74]: arr2d[0][2]
Out[74]: 3
In [75]: arr2d[0, 2]
Out[75]: 3
在多維數組中,如果省略了後面的索引,則返回對象會是一個維度低一點的 ndarray(它含有高一級維度上的所有數據)。因此,在 2×2×3 數組 arr3d 中:
In [76]: arr3d = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])
In [77]: arr3d
Out[77]:
array([[[ 1, 2, 3],
[ 4, 5, 6]],
[[ 7, 8, 9],
[10, 11, 12]]])
arr3d[0] 是一個 2×3 數組:
In [78]: arr3d[0]
Out[78]:
array([[1, 2, 3],
[4, 5, 6]])
標量值和數組都可以被賦值給 arr3d[0]:
In [79]: old_values = arr3d[0].copy()
In [80]: arr3d[0] = 42
In [81]: arr3d
Out[81]:
array([[[42, 42, 42],
[42, 42, 42]],
[[ 7, 8, 9],
[10, 11, 12]]])
In [82]: arr3d[0] = old_values
In [83]: arr3d
Out[83]:
array([[[ 1, 2, 3],
[ 4, 5, 6]],
[[ 7, 8, 9],
[10, 11, 12]]])
類似的,arr3d[1, 0]返回的是一個一維數組:
In [84]: arr3d[1, 0]
Out[84]: array([7, 8, 9])
上面的表達式可以分解爲下面兩步:
In [85]: x = arr3d[1]
In [86]: x
Out[86]:
array([[ 7, 8, 9],
[10, 11, 12]])
In [87]: x[0]
Out[87]: array([7, 8, 9])
1.4.1 數組的切片索引
ndarray 的切片語法跟 Python 列表這樣的一維對象差不多:
In [88]: arr
Out[88]: array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])
In [89]: arr[1:6]
Out[89]: array([ 1, 2, 3, 4, 64])
再回想下前面的二維數組,arr2d,對數組進行切片略有不同:
In [90]: arr2d
Out[90]:
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
In [91]: arr2d[:2]
Out[91]:
array([[1, 2, 3],
[4, 5, 6]])
可以看出,它是沿着第 0 軸(即第一個軸)切片的。也就是說,切片是沿着一個軸向選取元素的。表達式 arr2d[:2] 可以被認爲是 “選取 arr2d 的前兩行”。
你可以一次傳入多個切片,就像傳入多個索引那樣:
In [92]: arr2d[:2, 1:]
Out[92]:
array([[2, 3],
[5, 6]])
當你像上面的這個例子中那樣切片時,你需要按照原數組的維度進行切片。如果將索引和切片混合,就可以得到低緯度的切片。
例如,我可以選擇第二行但是隻選擇前兩列:
In [93]: arr2d[1, :2]
Out[93]: array([4, 5])
類似的,我也可以選擇第三列,但是隻選擇前兩行:
In [94]: arr2d[:2, 2]
Out[94]: array([3, 6])
需要注意的是,單獨一個冒號表示選取整個軸上的數據,因此你可以像下面這樣在更高維度進行切片:
In [95]: arr2d[:, :1]
Out[95]:
array([[1],
[4],
[7]])
當然對切片表達式賦值時,整個切片都會重新賦值:
In [96]: arr2d[:2, 1:] = 0
In [97]: arr2d
Out[97]:
array([[1, 0, 0],
[4, 0, 0],
[7, 8, 9]])
1.5 布爾索引
讓我們考慮以下例子,假設我們的數據都在數組中,並且數組中的數據是一些存在重複的人名。我會使用numpy.random
中的randn
函數來生成一些隨機正態分佈的數據:
In [98]: names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
In [99]: data = np.random.randn(7, 4)
In [100]: names
Out[100]:
array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'],
dtype='<U4')
In [101]: data
Out[101]:
array([[ 0.0929, 0.2817, 0.769 , 1.2464],
[ 1.0072, -1.2962, 0.275 , 0.2289],
[ 1.3529, 0.8864, -2.0016, -0.3718],
[ 1.669 , -0.4386, -0.5397, 0.477 ],
[ 3.2489, -1.0212, -0.5771, 0.1241],
[ 0.3026, 0.5238, 0.0009, 1.3438],
[-0.7135, -0.8312, -2.3702, -1.8608]])
假設每個人名都和data數組中的一行相對應,並且我們想要選中所有的‘Bob’對應的行。與數學操作類似,數組的比較操作(比如==)也是可以向量化的。因此,比較names
數組和字符串‘Bob’會產生一個布爾值數組:
In [102]: names == 'Bob'
Out[102]: array([ True, False, False, True, False, False, False], dtype=bool)
在索引數組時可以傳入布爾值數組:
In [103]: data[names == 'Bob']
Out[103]:
array([[ 0.0929, 0.2817, 0.769 , 1.2464],
[ 1.669 , -0.4386, -0.5397, 0.477 ]])
上面代碼的意思是,在names
數組中,第0個和第3個爲‘Bob’,所以爲True,傳入data後,則選擇了第0行和第3行的數據。
布爾值數組的長度必須和數組軸索引長度一致。你甚至還可以用切片或整數值(或整數值的序列)對布爾值數組進行混合和匹配。
這裏解釋一下,上述例子中,布爾值數組長度爲7,數組軸索引長度也爲7。
注意:當布爾值數組的長度不正確時,布爾值選擇數據的方法並不會報錯,因此在使用該特性的時候要小心。
下面的例子,我選取了names == 'Bob'
的行,並索引了列:
In [104]: data[names == 'Bob', 2:]
Out[104]:
array([[ 0.769 , 1.2464],
[-0.5397, 0.477 ]])
In [105]: data[names == 'Bob', 3]
Out[105]: array([ 1.2464, 0.477 ])
爲了選擇除了‘Bob’以外的其他數據,你可以使用!=或在條件表達式前使用~對條件取反:
In [106]: names != 'Bob'
Out[106]: array([False, True, True, False, True, True, True], dtype=bool)
In [107]: data[~(names == 'Bob')]
Out[107]:
array([[ 1.0072, -1.2962, 0.275 , 0.2289],
[ 1.3529, 0.8864, -2.0016, -0.3718],
[ 3.2489, -1.0212, -0.5771, 0.1241],
[ 0.3026, 0.5238, 0.0009, 1.3438],
[-0.7135, -0.8312, -2.3702, -1.8608]])
~符號可以在你想要對一個通用條件取反時使用:
In [108]: cond = names == 'Bob'
In [109]: data[~cond]
Out[109]:
array([[ 1.0072, -1.2962, 0.275 , 0.2289],
[ 1.3529, 0.8864, -2.0016, -0.3718],
[ 3.2489, -1.0212, -0.5771, 0.1241],
[ 0.3026, 0.5238, 0.0009, 1.3438],
[-0.7135, -0.8312, -2.3702, -1.8608]])
選取這三個名字中的兩個需要組合應用多個布爾條件,使用 &(和)、|(或)之類的布爾算術運算符即可:
In [110]: mask = (names == 'Bob') | (names == 'Will')
In [111]: mask
Out[111]: array([ True, False, True, True, True, False, False], dtype=bool)
In [112]: data[mask]
Out[112]:
array([[ 0.0929, 0.2817, 0.769 , 1.2464],
[ 1.3529, 0.8864, -2.0016, -0.3718],
[ 1.669 , -0.4386, -0.5397, 0.477 ],
[ 3.2489, -1.0212, -0.5771, 0.1241]])
引用布爾值索引選擇數據時,總是生成數據的拷貝,即使返回的數組並沒有任何變化。
注意:Python的關鍵字and和or對布爾值數組並沒有作用,請使用&(and)和 |(or)來代替。
基於常識來設置布爾值數組的值也是可以的。將data中所有的負值設置成0,我們需要做:
In [113]: data[data < 0] = 0
In [114]: data
Out[114]:
array([[ 0.0929, 0.2817, 0.769 , 1.2464],
[ 1.0072, 0. , 0.275 , 0.2289],
[ 1.3529, 0.8864, 0. , 0. ],
[ 1.669 , 0. , 0. , 0.477 ],
[ 3.2489, 0. , 0. , 0.1241],
[ 0.3026, 0.5238, 0.0009, 1.3438],
[ 0. , 0. , 0. , 0. ]])
利用一維布爾值數組對每一行設置數值也是非常簡單的:
In [115]: data[names != 'Joe'] = 7
In [116]: data
Out[116]:
array([[ 7. , 7. , 7. , 7. ],
[ 1.0072, 0. , 0.275 , 0.2289],
[ 7. , 7. , 7. , 7. ],
[ 7. , 7. , 7. , 7. ],
[ 7. , 7. , 7. , 7. ],
[ 0.3026, 0.5238, 0.0009, 1.3438],
[ 0. , 0. , 0. , 0. ]])
後面會看到,這類二維數據的操作也可以用 pandas 方便的來做。
1.6 神奇的索引
神奇索引是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.]])
如果使用負的索引,將從尾部進行選擇:
In [121]: arr[[-3, -5, -7]]
Out[121]:
array([[ 5., 5., 5., 5.],
[ 3., 3., 3., 3.],
[ 1., 1., 1., 1.]])
傳遞多個索引數組時情況有些許不同,這樣會根據每個索引元組對應的元素選出一個一維數組:
In [122]: arr = np.arange(32).reshape((8, 4))
In [123]: arr
Out[123]:
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15],
[16, 17, 18, 19],
[20, 21, 22, 23],
[24, 25, 26, 27],
[28, 29, 30, 31]])
In [124]: arr[[1, 5, 7, 2], [0, 3, 1, 2]]
Out[124]: array([ 4, 23, 29, 10])
最終選出的是元素 (1,0)、(5,3)、(7,1) 和(2,2)。如果不考慮數組的維數,神奇索引的結果總是一維的。
神奇索引的行爲可能會跟某些用戶的預期不一樣(包括我在內),選取矩陣的行列子集應該是矩形區域的形式纔對。下面是得到該結果的一個辦法:
In [125]: arr[[1, 5, 7, 2]][:, [0, 3, 1, 2]]
Out[125]:
array([[ 4, 7, 5, 6],
[20, 23, 21, 22],
[28, 31, 29, 30],
[ 8, 11, 9, 10]])
請牢記神奇索引與切片不同,它總是將數據複製到一個新的數組中。
1.7 數組轉置和換軸
轉置是一種特殊的數據重組形式,可以返回底層數據的視圖而不需要複製任何內容。數組擁有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]])
可以發現,原數組的行和列進行了互換。
當進行矩陣操作時,有事還會用到一些特定操作。如計算矩陣內積會使用np.dot
:
In [129]: arr = np.random.randn(6, 3)
In [130]: arr
Out[130]:
array([[-0.8608, 0.5601, -1.2659],
[ 0.1198, -1.0635, 0.3329],
[-2.3594, -0.1995, -1.542 ],
[-0.9707, -1.307 , 0.2863],
[ 0.378 , -0.7539, 0.3313],
[ 1.3497, 0.0699, 0.2467]])
In [131]: np.dot(arr.T, arr)
Out[131]:
array([[ 9.2291, 0.9394, 4.948 ],
[ 0.9394, 3.7662, -1.3622],
[ 4.948 , -1.3622, 4.3437]])
對於更高維度的數組,transpose
方法可以接受包含軸編號的元組,用於置換軸:
In [132]: arr = np.arange(16).reshape((2, 2, 4))
In [133]: arr
Out[133]:
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7]],
[[ 8, 9, 10, 11],
[12, 13, 14, 15]]])
In [134]: arr.transpose((1, 0, 2))
Out[134]:
array([[[ 0, 1, 2, 3],
[ 8, 9, 10, 11]],
[[ 4, 5, 6, 7],
[12, 13, 14, 15]]])
這裏,軸已經被重新排序,使得原先的第二個軸變爲第一個,原先的第一個軸變成第二個,最後一個軸並沒有變。
使用.T
進行轉置是換軸的一個特殊案例。ndarray有一個swapaxes
方法,該方法接受一對編號作爲參數,並對軸進行調整用於重組數據:
In [135]: arr
Out[135]:
array([[[ 0, 1, 2, 3],
[ 4, 5, 6, 7]],
[[ 8, 9, 10, 11],
[12, 13, 14, 15]]])
In [136]: arr.swapaxes(1, 2)
Out[136]:
array([[[ 0, 4],
[ 1, 5],
[ 2, 6],
[ 3, 7]],
[[ 8, 12],
[ 9, 13],
[10, 14],
[11, 15]]])
swapaxes
也是返回源數據的視圖(不會進行任何複製操作)。
2. 通用函數:快速的逐元素數組函數
通用函數,也可以成爲ufunc
,是一種ndarray數據中進行逐元素操作的函數。某些簡單函數接收一個或多個標量數值,併產生一個或多個標量結果,而通用函數就是對這些簡單函數的向量化封裝。
有很多ufunc是簡單的逐元素轉換,比如sqrt
或exp
函數:
In [137]: arr = np.arange(10)
In [138]: arr
Out[138]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
In [139]: np.sqrt(arr)
Out[139]:
array([ 0. , 1. , 1.4142, 1.7321, 2. , 2.2361, 2.4495,
2.6458, 2.8284, 3. ])
In [140]: np.exp(arr)
Out[140]:
array([ 1. , 2.7183, 7.3891, 20.0855, 54.5982,
148.4132, 403.4288, 1096.6332, 2980.958 , 8103.0839])
這是所謂的一元通用函數。還有一些通用函數,比如add
或maximum
則會接收兩個數組並返回一個數組作爲結果,因此稱爲二元通用函數:
In [141]: x = np.random.randn(8)
In [142]: y = np.random.randn(8)
In [143]: x
Out[143]:
array([-0.0119, 1.0048, 1.3272, -0.9193, -1.5491, 0.0222, 0.7584,
-0.6605])
In [144]: y
Out[144]:
array([ 0.8626, -0.01 , 0.05 , 0.6702, 0.853 , -0.9559, -0.0235,
-2.3042])
In [145]: np.maximum(x, y)
Out[145]:
array([ 0.8626, 1.0048, 1.3272, 0.6702, 0.853 , 0.0222, 0.7584,
-0.6605])
這裏,numpy.maximum
逐個元素地將x和y中元素的最大值計算出來。
也有一些通用函數返回多個數組。比如modf
,是Python內建函數divmod
的向量化版本。它返回了一個浮點值數組的小數部分和整數部分:
In [146]: arr = np.random.randn(7) * 5
In [147]: arr
Out[147]: array([-3.2623, -6.0915, -6.663 , 5.3731, 3.6182, 3.45 , 5.0077])
In [148]: remainder, whole_part = np.modf(arr)
In [149]: remainder
Out[149]: array([-0.2623, -0.0915, -0.663 , 0.3731,
0.6182, 0.45 , 0.0077])
In [150]: whole_part
Out[150]: array([-3., -6., -6., 5., 3., 3., 5.])
通用函數接收一個可選參數out
,允許對數組按位置操作:
In [151]: arr
Out[151]: array([-3.2623, -6.0915, -6.663 , 5.3731, 3.6182, 3.45 , 5.0077])
In [152]: np.sqrt(arr)
Out[152]: array([ nan, nan, nan, 2.318 , 1.9022, 1.8574, 2.2378])
In [153]: np.sqrt(arr, arr)
Out[153]: array([ nan, nan, nan, 2.318 , 1.9022, 1.8574, 2.2378])
In [154]: arr
Out[154]: array([ nan, nan, nan, 2.318 , 1.9022, 1.8574, 2.2378])
講真這部分我沒太看懂,是說通過將arr傳入兩次,更改了原arr的數據內容麼?
表: 一元通用函數
函數名 | 描述 |
---|---|
abs、fabs | 逐元素地計算整數、浮點數或複數的絕對值 |
sqrt | 計算每個元素的平方根(與arr ** 0.5相等) |
square | 計算每個元素的平方(與arr**2相等) |
exp | 計算每個元素的自然指數值e**x |
log、log10、log2、log1p | 分別對應:自然對數(e爲底)、對數10爲底、對數2爲底、log(1+x) |
sign | 計算每個元素的符號值:1(正數)、0(0)、-1(負數) |
ceil | 計算每個元素的最高整數值(即大於等於給定數值的最小整數) |
floor | 計算每個元素的最小整數值(即小於等於給定數值的最大整數) |
modf | 分別將數組中的小數部分和整數部分按數組形式返回 |
isnan | 返回數組中的元素是否是一個NaN(不是一個數值),形式爲布爾值數組 |
isfinite、isinf | 分別返回數組中的元素是否有限(非inf、非NaN)、是否無限的,形式爲布爾值數組 |
cos、cosh、sin、sinh、tan、tanh | 常規的三角函數 |
arccos、arccosh、arcsin、arcsinh、arctan、arctanh | 反三角函數 |
logical_not | 對數組的元素按位取反(與~arr效果一樣) |
表: 二元通用函數
函數名 | 描述 |
---|---|
add | 將數組的對應元素增加 |
subtract | 在第二個數組中,將第一個數組中包含的元組去除 |
multiply | 將數組的對應元素相乘 |
divide, floor_divide | 除或整除(放棄餘數) |
power | 將第二個數組的元素作爲第一個數組對應元素的冪次方 |
maximum, fmax | 逐個元素計算最大值,fmax忽略NaN |
minimum, fmin | 逐個元素計算最小值,fmax忽略NaN |
mod | 按元素的求模計算(即求除法的餘數) |
copysign | 將第一個數組的符號值改爲第二個數組的符號值 |
greater, greater_equal, less, less_equal, equal, not_equal, logical_and, logical_or, logical_xor | 進行逐個元素的比較,返回布爾值數組(與數學操作符>、>=、<、<=、==、!= 效果一致)進行逐個元素的邏輯操作(與邏輯操作符&、|、^ 效果一致) |
3. 使用數組進行面向數組編程
利用數組表達式來替代顯示循環的方法,稱爲向量化。通常,向量化的數組操作會比純Python的等價實現在速度上快一到兩個數量級,這對所有種類的數值計算產生了最大的影響。
作爲簡單的例子,假設我們想要在一組值(網格型)上計算函數sqrt(x^2 + y^2)。np.meshgrid
函數接受兩個一維數組,併產生兩個二維矩陣(對應於兩個數組中所有的 (x,y) 對):
In [155]: points = np.arange(-5, 5, 0.01) # 1000 equally spaced points
In [156]: xs, ys = np.meshgrid(points, points)
In [157]: ys
Out[157]:
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]])
現在,你可以利用和兩個座標值同樣的表達式來使用函數:
In [158]: z = np.sqrt(xs ** 2 + ys ** 2)
In [159]: z
Out[159]:
array([[ 7.0711, 7.064 , 7.0569, ..., 7.0499, 7.0569, 7.064 ],
[ 7.064 , 7.0569, 7.0499, ..., 7.0428, 7.0499, 7.0569],
[ 7.0569, 7.0499, 7.0428, ..., 7.0357, 7.0428, 7.0499],
...,
[ 7.0499, 7.0428, 7.0357, ..., 7.0286, 7.0357, 7.0428],
[ 7.0569, 7.0499, 7.0428, ..., 7.0357, 7.0428, 7.0499],
[ 7.064 , 7.0569, 7.0499, ..., 7.0428, 7.0499, 7.0569]])
3.1 將條件邏輯作爲數組操作
numpy.where
函數式三元表達式x if condition else y
的向量化版本。假設我們有一個布爾值數組和兩個數值數組:
In [165]: xarr = np.array([1.1, 1.2, 1.3, 1.4, 1.5])
In [166]: yarr = np.array([2.1, 2.2, 2.3, 2.4, 2.5])
In [167]: cond = np.array([True, False, True, True, False])
假設cond中的元素爲True時,我們取axrr中的對應元素值,否則取yarr中的元素值,可以通過以上代碼完成。但如果數組很大,速度會很慢,而使用np.where
時,就可以非常簡單地完成:
In [168]: result = [(x if c else y)
.....: for x, y, c in zip(xarr, yarr, cond)]
In [169]: result
Out[169]: [1.1000000000000001, 2.2000000000000002, 1.3, 1.3999999999999999, 2.5]
這樣會產生很多問題。首先,如果數組很大的話,速度會很慢(因爲所有的工作都是通過解釋器解釋Python代碼完成)。其次,當數組是多維時,就無法奏效了。而使用np.where
時,就可以非常簡單地完成:
In [170]: result = np.where(cond, xarr, yarr)
In [171]: result
Out[171]: array([ 1.1, 2.2, 1.3, 1.4, 2.5])
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]])
你可以使用np.where
將標量和數組聯合,例如,我可以像下面的代碼那樣將arr中所有正值都替換爲常數2:
In [176]: np.where(arr > 0, 2, arr) # set only positive values to 2
Out[176]:
array([[-0.5031, -0.6223, -0.9212, -0.7262],
[ 2. , 2. , -1.1577, 2. ],
[ 2. , 2. , 2. , -0.9975],
[ 2. , -0.1316, 2. , 2. ]])
3.2 數學和統計方法
可以通過數組上的一組數學函數對整個數組或某個軸向的數據進行統計計算。sum、mean 以及標準差 std 等聚合計算(aggregation,通常也叫做縮減函數(reduction))既可以當做數組的實例方法調用,也可以當做頂級 NumPy 函數使用。
這裏,我生成了一些正態分佈隨機數據,然後做了聚類統計:
In [177]: arr = np.random.randn(5, 4)
In [178]: arr
Out[178]:
array([[ 2.1695, -0.1149, 2.0037, 0.0296],
[ 0.7953, 0.1181, -0.7485, 0.585 ],
[ 0.1527, -1.5657, -0.5625, -0.0327],
[-0.929 , -0.4826, -0.0363, 1.0954],
[ 0.9809, -0.5895, 1.5817, -0.5287]])
In [179]: arr.mean()
Out[179]: 0.19607051119998253
In [180]: np.mean(arr)
Out[180]: 0.19607051119998253
In [181]: arr.sum()
Out[181]: 3.9214102239996507
像mean
、sum
等函數可以接收一個可選參數axis
,這個參數可以用於計算給定軸向上的統計值,形成一個下降一維度的數組:
In [182]: arr.mean(axis=1)
Out[182]: array([ 1.022 , 0.1875, -0.502 , -0.0881, 0.3611])
In [183]: arr.sum(axis=0)
Out[183]: array([ 3.1693, -2.6345, 2.2381, 1.1486])
這裏,arr.mean(1)是 “計算行的平均值”,arr.sum(0) 是“計算每列的和”。
其他如 cumsum
和cumprod
之類的方法則不聚合,而是產生一個由中間結果組成的數組:
In [184]: arr = np.array([0, 1, 2, 3, 4, 5, 6, 7])
In [185]: arr.cumsum()
Out[185]: array([ 0, 1, 3, 6, 10, 15, 21, 28])
在多維數組中,像cumsum
這樣的累積函數返回相同長度的數組,但是可以在指定軸向上根據維度的切片進行部分聚合:
In [186]: arr = np.array([[0, 1, 2], [3, 4, 5], [6, 7, 8]])
In [187]: arr
Out[187]:
array([[0, 1, 2],
[3, 4, 5],
[6, 7, 8]])
In [188]: arr.cumsum(axis=0)
Out[188]:
array([[ 0, 1, 2],
[ 3, 5, 7],
[ 9, 12, 15]])
In [189]: arr.cumprod(axis=1)
Out[189]:
array([[ 0, 0, 0],
[ 3, 12, 60],
[ 6, 42, 336]])
表: 基礎數組統計方法
方法 | 描述 |
---|---|
sum | 沿着軸向計算所有元素的累和,0長度的數組,累和爲0 |
mean | 數學平均,0長度的數組平均值爲NaN |
std, var | 標準差和方差,可以選擇自由度調整(默認分母是n) |
min, max | 最大值和最小值 |
argmin, argmax | 最大值和最小值的位置 |
cumsum | 從0開始元素累積和 |
cumprod | 從1開始元素累積積 |
3.3 布爾值數組的方法
前面介紹的方法,布爾值會被強制爲1(True)和0(False)。因此,sum
通常可以用於計算布爾值數組中的True的個數:
In [190]: arr = np.random.randn(100)
In [191]: (arr > 0).sum() # Number of positive values
Out[191]: 42
注意!此處的sum
是統計大於0的數字的個數,而不是大於0的數字的和。
對於布爾值數組,有兩個非常有用的方法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處理。
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 ]])
頂層的np.sort
方法返回的是已經排序好的數組拷貝,而不是對原數組按位置排序。可以向sort()方法中傳入參數0或1,傳入0時按列排序,傳入1時按行排序,都是由小到大進行排列。
下面的例子計算的是一個數組的分位數,並選出分位數所對應的數,這是一種應急的方式:
In [203]: large_arr = np.random.randn(1000)
In [204]: large_arr.sort()
In [205]: large_arr[int(0.05 * len(large_arr))] # 5% quantile
Out[205]: -1.5311513550102103
3.5 唯一值與其他集合邏輯
Numpy包含一些針對一維ndarray的基礎集合操作。常用的一個方法是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.unique
等價的純 Python 代碼來對比一下:
In [210]: sorted(set(names))
Out[210]: ['Bob', 'Joe', 'Will']
另一個函數,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)
表 數組集合操作
方法 | 描述 |
---|---|
unique(x) | 計算x的唯一值,並排序 |
intersect1d(x, y) | 計算x和y的交集,並排序 |
union1d(x, y) | 計算x和y的並集,並排序 |
in1d(x, y) | 計算x中的元素是否包含在y中,返回一個布爾值數組 |
setdiff1d(x, y) | 差集,在x中但不在y中的x的元素 |
setxor1d(x,y) | 異或集,在x或y中,但不屬於x、y交集的元素 |
4. 使用數組進行文件輸入和輸出
Numpy可以在硬盤中將數據以文本或二進制文件的形式進行存入硬盤或由硬盤載入。
np.save
和np.load
是高效存取硬盤數據的兩大工具函數。數組在默認情況下是以未壓縮的格式進行存儲的,後綴名是 .npy:
In [213]: arr = np.arange(10)
In [214]: np.save('some_array', arr)
如果文件存放路徑中沒寫 .npy時,後綴名會被自動加上。硬盤上的數組可以使用np.load
進行載入:
In [215]: np.load('some_array.npy')
Out[215]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
我們可以使用np.savez
並將數組作爲參數傳遞給該函數,用於在未壓縮文件中保存多個數組:
In [216]: np.savez('array_archive.npz', a=arr, b=arr)
當載入一個.npy文件的時候,我們會獲得一個字典型的對象,並通過該對象很方便地載入單個數組:
In [217]: arch = np.load('array_archive.npz')
In [218]: arch['b']
Out[218]: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
如果要將數據壓縮,可以使用 numpy.savez_compressed
:
In [219]: np.savez_compressed('arrays_compressed.npz', a=arr, b=arr)
5. 線性代數
線性代數,比如矩陣乘法、分解、行列式等方陣數學,是所有數組類庫的重要組成部分。和Matlab等其他語言相比,Numpy的線性代數中所不同的是 *是矩陣的逐元素乘積,而不是矩陣的點乘積。因此Numpy的數組方法和numpy命名空間中都有一個函數dot
,用於矩陣的操作:
In [223]: x = np.array([[1., 2., 3.], [4., 5., 6.]])
In [224]: y = np.array([[6., 23.], [-1, 7], [8, 9]])
In [225]: x
Out[225]:
array([[ 1., 2., 3.],
[ 4., 5., 6.]])
In [226]: y
Out[226]:
array([[ 6., 23.],
[ -1., 7.],
[ 8., 9.]])
In [227]: x.dot(y)
Out[227]:
array([[ 28., 64.],
[ 67., 181.]])
x.dot(y)等價於np.dot(x, y):
In [228]: np.dot(x, y)
Out[228]:
array([[ 28., 64.],
[ 67., 181.]])
一個二維數組和一個長度合適的一維數組之間的矩陣乘積,其結果是一個一維數組:
In [229]: np.dot(x, np.ones(3))
Out[229]: array([ 6., 15.])
特殊符號@也作爲中綴操作符,用於點乘矩陣操作:
In [230]: x @ np.ones(3)
Out[230]: array([ 6., 15.])
numpy.linalg
擁有一個矩陣分解的標準函數集,以及其他常用函數,例如求逆和行列式求解。這些函數都是通過在MATLAB和R等其他語言使用的相同的行業標準線性代數庫來實現的,例如BLAS、LAPACK或英特爾專用的MKL(數學核心庫):
In [231]: from numpy.linalg import inv, qr
In [232]: X = np.random.randn(5, 5)
In [233]: mat = X.T.dot(X)
In [234]: inv(mat)
Out[234]:
array([[ 933.1189, 871.8258, -1417.6902, -1460.4005, 1782.1391],
[ 871.8258, 815.3929, -1325.9965, -1365.9242, 1666.9347],
[-1417.6902, -1325.9965, 2158.4424, 2222.0191, -2711.6822],
[-1460.4005, -1365.9242, 2222.0191, 2289.0575, -2793.422 ],
[ 1782.1391, 1666.9347, -2711.6822, -2793.422 , 3409.5128]])
In [235]: mat.dot(inv(mat))
Out[235]:
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.]])
In [236]: q, r = qr(mat)
In [237]: r
Out[237]:
array([[-1.6914, 4.38 , 0.1757, 0.4075, -0.7838],
[ 0. , -2.6436, 0.1939, -3.072 , -1.0702],
[ 0. , 0. , -0.8138, 1.5414, 0.6155],
[ 0. , 0. , 0. , -2.6445, -2.1669],
[ 0. , 0. , 0. , 0. , 0.0002]])
表達式 X.T.dot(X) 計算 X 和它的轉置 X.T 的點積。
表:常用numpy.linalg函數
函數 | 描述 |
---|---|
diag | 將一個方陣的對角(或非對角)元素作爲一維數組返回,或者將一維數組轉換成一個方陣,並且在非對角線上有零點 |
dot | 矩陣點乘 |
trace | 計算對角元素和 |
det | 計算矩陣的行列式 |
eig | 計算方陣的特徵值和特徵向量 |
inv | 計算方陣的逆矩陣 |
pinv | 計算矩陣的Moore-Penrose僞逆 |
qr | 計算QR分解 |
svd | 計算奇異值分解(SVD) |
solve | 求解x的線性系統Ax=b,其中A是方陣 |
lstsq | 計算Ax=b的最小二乘解 |
6. 僞隨機數生成
numpy.random
模塊填補了Python內建的random模塊的不足,可以高效地生成多種概率分佈下的完整樣本值數組。例如,可以使用normal
來獲得一個4X4的正態分佈樣本數組:
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 ]])
然而Python內建的random
模塊一次只能生成一個值。你可以從下面的示例中看到,numpy.random
在生成大型樣本時比純Python的方式快了一個數量級:
In [240]: from random import normalvariate
In [241]: N = 1000000
In [242]: %timeit samples = [normalvariate(0, 1) for _ in range(N)]
1.77 s +- 126 ms per loop (mean +- std. dev. of 7 runs, 1 loop each)
In [243]: %timeit np.random.normal(size=N)
61.7 ms +- 1.32 ms per loop (mean +- std. dev. of 7 runs, 10 loops each)
我們說這些都是僞隨機數,是因爲它們都是通過算法基於隨機數生成器種子,在確定性的條件下生成的。你可以用 NumPy 的 np.random.seed
更改隨機數生成種子:
In [244]: np.random.seed(1234)
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])
表:numpy.random中的部分函數列表
函數 | 描述 |
---|---|
seed | 向隨機數生成器傳遞隨機狀態種子 |
permutation | 返回一個序列的隨機排列,或者返回一個亂序的整數範圍序列 |
shuffle | 隨機排列一個序列 rand 從均勻分佈中抽取樣本 |
randint | 根據給定的由低到高的範圍抽取隨機整數 |
randn | 從均值0方差1的正態分佈中抽取樣本(MATLAB型接口) |
binomial | 從二項分佈中抽取樣本 |
normal | 從正態(高斯)分佈中抽取樣本 |
beta | 從beta分佈中抽取樣本 |
chisquare | 從卡方分佈中抽取樣本 |
gamma | 從伽馬分佈中抽取樣本 |
uniform | 從均勻[0,1)分佈中抽取樣本 |
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)
.....:
下圖是對上面隨機漫步的前100步的可視化:
你可能會觀察到walk只是對隨機步進的累積,並且可以通過一個數組表達式實現。因爲,我使用np.random
模塊一次性抽取1,000次擲硬幣的結果,每次投擲的結果爲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()
由此我們開始從漫步軌道上提取一些統計數據,比如最大值、最小值等:
In [255]: walk.min()
Out[255]: -3
In [256]: walk.max()
Out[256]: 31
更復雜的統計是第一次穿越時間,即隨機漫步的某一步達到了某個特定值。這裏假設我們想要知道漫步中是合適連續朝某個方向連續走了10步。np.abs(walk)>=10
給我們一個布爾值數組,用於表明漫步是否連續在同一方向走了十步,但是我們想要的是第一次走了10步或 -10步的位置。我們可以使用argmax
來計算,該函數可以返回布爾值數組中最大值的第一個位置(True就是最大值):
In [257]: (np.abs(walk) >= 10).argmax()
Out[257]: 37
請注意,這裏使用argmax
的效率並不高,因爲它總是完整地掃描整個數組。在這個特殊的示例中,一旦True被發現,我們就知道最大值了。
7.1 一次性模擬多次隨機漫步
如果你的目標是模擬多次隨機漫步,比如說5000步。你可以稍微修改下之前的代碼來生成所有的隨機步。如果傳入一個2個元素的元組,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]])
得到這些數據之後,我們來計算 30 或-30 的最小穿越時間。這裏稍微複雜些,因爲不是 5000 個過程都到達了 30。我們可以用 any 方法來對此進行檢查:
In [266]: hits30 = (np.abs(walks) >= 30).any(1)
In [267]: hits30
Out[267]: array([False, True, False, ..., False, True, False], dtype=bool)
In [268]: hits30.sum() # Number that hit 30 or -30
Out[268]: 3410
我們可以使用布爾值數組來選出絕對步數超過30的步所在的行,並使用argmax
從軸向1上獲取穿越時間:
In [269]: crossing_times = (np.abs(walks[hits30]) >= 30).argmax(1)
In [270]: crossing_times.mean()
Out[270]: 498.88973607038122
利用其它分佈而不是等概率的擲硬幣實驗來隨機漫步也是很容易的。你只需要使用一個不同的隨機數生成函數,比如normal
,在根據特定的均值和標準差即可生成正態分佈下的隨機步:
In [271]: steps = np.random.normal(loc=0, scale=0.25,
.....: size=(nwalks, nsteps))