NumPy基礎:多維數組

NumPy(Numerical Python的基礎)是高性能科學計算和數據分析的基礎包。其部分功能如下:

  • ndarray,一個具有矢量算術運算和複雜廣播能力的快速且節省空間的多維數組。
  • 用於對數組數據進行快速運算的標準數學函數(無序編寫循環)。
  • 用於讀寫磁碟數據的工具及其用於操作內存映射文件的工具。
  • 線性代數、隨機數生成以及傅里葉變換功能。
  • 用於集成由C、C++、Fortran等語言編寫的代碼的工具。

創建ndarray

創建數組最簡單的辦法就是使用 array函數。它接受一切序列型的對象(包括其他數組),然後產生一個新的含有傳入數據的NumPy數組。

列表的轉換:

  1. data1 = [6,7.5,8,0,1]
  2. arr1 = np.array(data1)
  3. # array([ 6. , 7.5, 8. , 0. , 1. ])

嵌套序列(比如由一組等長列表組成的列表)將會被轉為一個多維數組:

  1. data2 = [[1,2,3,4],[5,6,7,8]]
  2. arr2 = np.array(data2)
  3. # array([[1, 2, 3, 4],
  4. # [5, 6, 7, 8]])

data2是一個list of lists, 所以arr2維度為2。我們能用ndim和shape屬性來確認一下:

  1. arr2.ndim
  2. # 2
  1. arr2.shape
  2. # (2,4)

除非主動聲明,否則np.array會自動給data搭配適合的類型,並保存在dtype里:

  1. arr1.dtype
  2. # dtype(float64)
  1. arr2.dtype
  2. # dtype(int64)

除了np.array,還有一些其他函數能創建數組。比如zeros,ones,另外還可以在一個tuple里指定shape:

  1. np.zeros(10)
  2. # array([ 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
  1. np.zeros((3,6))
  2. # array([[ 0., 0., 0., 0., 0., 0.],
  3. # [ 0., 0., 0., 0., 0., 0.],
  4. # [ 0., 0., 0., 0., 0., 0.]])
  1. np.empty((2,3,2))
  2. # array([[[ 0.00000000e+000, 0.00000000e+000],
  3. # [ 2.16538378e-314, 2.16514681e-314],
  4. # [ 2.16511832e-314, 2.16072529e-314]],
  5. # [[ 0.00000000e+000, 0.00000000e+000],
  6. # [ 2.14037397e-314, 6.36598737e-311],
  7. # [ 0.00000000e+000, 0.00000000e+000]]])

np.empty並不能保證返回所有是0的數組,某些情況下,會返回為初始化的垃圾數值,如上。

arange是一個數組版的python range函數:

  1. np.arange(15)
  2. # array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14])

一些創建數組的函數:

ndarray的數據類型

dtype保存數據的類型:

  1. arr1 = np.array([1, 2, 3], dtype=np.float64)
  2. arr2 = np.array([1, 2, 3], dtype=np.int32)
  3. arr1.dtype
  4. # dtype(float64)
  5. arr2.dtype
  6. # dtype(int32)

dtype才是numpy能靈活處理其他外界數據的原因。

可以用astype來轉換類型:

  1. arr = np.array([1, 2, 3, 4, 5])
  2. arr.dtype
  3. # dtype(int64)
  4. float_arr = arr.astype(np.float64)
  5. float_arr.dtype
  6. # dtype(float64)

上面是把int變為float。如果是把float變為int,小數點後的部分會被丟棄:

  1. arr = np.array([3.7, -1.2, -2.6, 0.5, 12.9, 10.1])
  2. arr
  3. # array([ 3.7, -1.2, -2.6, 0.5, 12.9, 10.1])
  4. arr.astype(np.int32)
  5. # array([ 3, -1, -2, 0, 12, 10], dtype=int32

還可以用astype把string里的數字變為實際的數字:

  1. numeric_strings = np.array([1.25, -9.6, 42], dtype=np.string_)
  2. numeric_strings
  3. # array([b1.25, b-9.6, b42],
  4. # dtype=|S4)
  5. numeric_strings.astype(float)
  6. # array([ 1.25, -9.6 , 42. ])

要十分注意numpy.string_類型,這種類型的長度是固定的,所以可能會直接截取部分輸入而不給警告。

如果轉換(casting)失敗的話,會給出一個ValueError提示。

可以用其他數組的dtype直接來制定類型:

  1. int_array = np.arange(10)
  2. calibers = np.array([.22, .270, .357, .380, .44, .50], dtype=np.float64)
  3. int_array.astype(calibers.dtype)
  4. # array([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])

還可以利用類型的縮寫,比如u4就代表unit32:

  1. empty_unit32 = np.empty(8, dtype=u4)
  2. empty_unit32
  3. # array([0, 0, 0, 0, 0, 0, 0, 0], dtype=uint32)

astype總是會返回一個新的數組。

數組和標量之間的運算

數組很重要,因為它使你不用編寫循環即可對數據執行批量運算。叫做矢量化。大小相等的數組之間的任何算術運算都會將運算應用到元素級

  1. arr = np.array([[1., 2., 3.], [4., 5., 6.]])
  2. arr
  3. # array([[ 1., 2., 3.],
  4. # [ 4., 5., 6.]]
  5. arr * arr
  6. # array([[ 1., 4., 9.],
  7. # [ 16., 25., 36.]])
  8. arr - arr
  9. # array([[ 0., 0., 0.],
  10. # [ 0., 0., 0.]])

數組與標量的算術運算也會將那個標量值傳播到各個元素:

  1. 1 / arr
  2. # array([[ 1. , 0.5 , 0.33333333],
  3. # [ 0.25 , 0.2 , 0.16666667]])
  4. arr ** 0.5
  5. # array([[ 1. , 1.41421356, 1.73205081],
  6. # [ 2. , 2.23606798, 2.44948974]])

不同大小的數組之間的運算叫做廣播(broadcasting)。

基本的索引和切片

NumPy數組的索引選取數據子集或單個元素的方式有很多。一維數組和Python列表功能差不多:

  1. arr = np.arange(10)
  2. arr
  3. # array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
  4. arr[5]
  5. # 5
  6. arr[5:8]
  7. # array([5, 6, 7])
  8. arr[5:8] = 12
  9. arr
  10. # array([ 0, 1, 2, 3, 4, 12, 12, 12, 8, 9])

當你將一個標量值賦給一個切片時(如arr[5:8]=12),該值會自動傳播(「廣播」)到整個選區。和Python列表不同的是,數組切片是原始數組的視圖。這意味著數據不會被複制,任何修改都會反應到源數組上。

  1. arr_slice = arr[5:8]
  2. arr_slice
  3. # array([12, 12, 12])
  4. arr_slice[1] = 12345
  5. # array([ 0, 1, 2, 3, 4, 12, 12345, 12, 8, 9])
  6. arr_slice[:] = 64
  7. arr
  8. # array([ 0, 1, 2, 3, 4, 64, 64, 64, 8, 9])

若想得到ndarray切片的一份副本,就需要顯式地進行複製操作,例如arr[5:8].copy()

在一個二維數組中,各索引位置上的元素不再是標量而是一維數組:

  1. arr2d = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
  2. arr2d[2]
  3. # array([7, 8, 9])

有兩種方式可以訪問單一元素:

  1. arr2d[0][2]
  2. # 3
  3. arr2d[0, 2]
  4. # 3

二維數組的索引方式:

對於多維數組,如果省略後面的索引,返回的將是一個低緯度的多維數組。例如,一個2 x 2 x 3數組

  1. arr3d = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])
  2. arr3d
  3. # array([[[ 1, 2, 3],
  4. # [ 4, 5, 6]],
  5. # [[ 7, 8, 9],
  6. # [10, 11, 12]]])

arr3d[0]是一個2x3數組:

  1. arr3d[0]
  2. # array([[1, 2, 3],
  3. # [4, 5, 6]])

標量和數組都能賦給arr3d[0]:

  1. old_values = arr3d[0].copy()
  2. arr3d[0] = 42
  3. arr3d
  4. # array([[[42, 42, 42],
  5. # [42, 42, 42]],
  6. # [[ 7, 8, 9],
  7. # [10, 11, 12]]])
  8. arr3d[0] = old_values
  9. arr3d
  10. # array([[[ 1, 2, 3],
  11. # [ 4, 5, 6]],
  12. # [[ 7, 8, 9],
  13. # [10, 11, 12]]])

arr3d[1, 0]會給返回一個(1, 0)的一維數組:

  1. arr3d[1, 0]
  2. # array([7, 8, 9])

注意,上述選取數組子集的例子返回的都是視圖(不是副本,是本尊)。

切片索引

ndarray的切片語法和Python列表這樣的一維對象差不多。

高維對象可以再一個或多個軸上進行切片,也可以跟整數索引混合使用。

  1. arr2d
  2. # array([[1,2,3],
  3. # [4,5,6]
  4. # [7,8,9]])
  5. arr2d[:2]
  6. # array([[1,2,3],
  7. # [4,5,6]])

可以看出,它是沿著axis 0(行)來處理的。可以一次傳入多個切片,就像傳入多個索引那樣:

  1. arr2d[:2, 1:]
  2. # array([[2, 3],
  3. # [5, 6]])

如此切片,只能得到相同維數的數組視圖。將整數索引和切片混合,可以得到低緯度切片:

  1. arr2d[1, :2]
  2. # array([4, 5])

注意,只有冒號表示選取整個軸,例如:

  1. arr2d[:, :1]
  2. # array([[1],
  3. # [4],
  4. # [7]])

對切片表達式的賦值操作也會擴散到整個選區:

  1. arr2d[:2, 1:] = 0
  2. arr2d
  3. # array([[1, 0, 0],
  4. # [4, 0, 0],
  5. # [7, 8, 9]])

布爾型索引

假設我們有一個用於存儲數據的數組以及一個存儲姓名的數組(含有重複項)。比如說:

  1. names = np.array([Bob, Joe, Will, Bob, Will, Joe, Joe])
  2. names
  3. # array([Bob, Joe, Will, Bob, Will, Joe, Joe],
  4. # dtype=<U4)
  5. data = np.random.randn(7, 4)
  6. data
  7. # array([[ 0.06226591, -0.27507719, 0.39229467, 1.0592541 ],
  8. # [ 0.29856009, -0.287806 , -1.06875432, -0.33292789],
  9. # [-0.48500348, -0.10072345, -1.76972263, -0.27355081],
  10. # [ 0.23004649, -0.76163183, 0.24673954, -0.47700137],
  11. # [ 1.54353606, -0.17964118, -0.7093982 , -1.55488714],
  12. # [ 0.17778785, 1.25049472, 1.92926838, 0.49794146],
  13. # [ 0.11571349, -1.28075539, -1.15407468, 0.86778147]])

假設每個名字對應data數組中的一行,我們想要選出對應於名字Bob的所有行。和算術運算一樣,數組的比較運算(如==)也是矢量化的。因此,對names和字元串「Bob」的比較運算會產生一個布爾型數組:

  1. names == Bob
  2. # array([ True, False, False, True, False, False, False], dtype=bool)

布爾型數組可用於數組索引:

  1. data[names == Bob]
  2. # array([[ 0.02584271, -1.53529621, 0.73143988, -0.34086189],
  3. # [-0.48632936, 0.63817756, -0.40792716, -1.48037389]])

布爾型數組的長度必須跟被索引的軸長度一致。還可以將布爾型數組跟切片、整數(或整數序列)混合使用:

  1. data[names == Bob, 2:]
  2. # array([[ 0.73143988, -0.34086189],
  3. # [-0.40792716, -1.48037389]])
  4. data[names == Bob, 3]
  5. # array([-0.34086189, -1.48037389])

選中除了Bob外的所有行,可以用!=或者~:

  1. names != Bob
  2. # array([False, True, True, False, True, True, True], dtype=bool)
  3. data[~(names == Bob)]
  4. # array([[ 0.40864782, 0.53476799, 1.09620596, 0.4846564 ],
  5. # [ 1.95024076, -0.37291038, -0.40424703, 0.30297059],
  6. # [-0.81976335, -1.10162466, -0.59823212, -0.10926744],
  7. # [-0.5212113 , 0.29449179, 2.0568032 , 2.00515735],
  8. # [-2.36066876, -0.3294302 , -0.24464646, -0.81432884]])

選取這三個名字中的兩個需要組合應用多個布爾條件,如 &(與)、 |(或)之類的布爾運算符。

  1. names
  2. # array([Bob, Joe, Will, Bob, Will, Joe, Joe],
  3. # dtype=<U4)
  4. mask = (names == Bob) | (names == Will)
  5. mask
  6. # array([ True, False, True, True, True, False, False], dtype=bool)
  7. data[mask]
  8. # array([[ 0.02584271, -1.53529621, 0.73143988, -0.34086189],
  9. # [ 1.95024076, -0.37291038, -0.40424703, 0.30297059],
  10. # [-0.48632936, 0.63817756, -0.40792716, -1.48037389],
  11. # [-0.81976335, -1.10162466, -0.59823212, -0.10926744]])

通過布爾型索引選取數組中的數據,總是創建數據的副本,即使返回一摸一樣的數組也是如此

通過布爾型數組設置值是一種常用手段,為了將data中的所有負值設置為0,只需:

  1. data[data < 0] = 0
  2. data
  3. # array([[ 0.02584271, 0. , 0.73143988, 0. ],
  4. # [ 0.40864782, 0.53476799, 1.09620596, 0.4846564 ],
  5. # [ 1.95024076, 0. , 0. , 0.30297059],
  6. # [ 0. , 0.63817756, 0. , 0. ],
  7. # [ 0. , 0. , 0. , 0. ],
  8. # [ 0. , 0.29449179, 2.0568032 , 2.00515735],
  9. # [ 0. , 0. , 0. , 0. ]])

通過一維布爾數組設置整行或列的值也很簡單:

  1. data[names != Joe] = 7
  2. data
  3. # array([[ 7. , 7. , 7. , 7. ],
  4. # [ 0.40864782, 0.53476799, 1.09620596, 0.4846564 ],
  5. # [ 7. , 7. , 7. , 7. ],
  6. # [ 7. , 7. , 7. , 7. ],
  7. # [ 7. , 7. , 7. , 7. ],
  8. # [ 0. , 0.29449179, 2.0568032 , 2.00515735],
  9. # [ 0. , 0. , 0. , 0. ]])

花式索引

花式索引是一個NumPy術語,它指的是利用整數數組進行索引。假設有一個8 x 4的數組:

  1. arr = np.empty((8, 4))
  2. for i in range(8):
  3. arr[i] = i
  4. arr
  5. # array([[ 0., 0., 0., 0.],
  6. # [ 1., 1., 1., 1.],
  7. # [ 2., 2., 2., 2.],
  8. # [ 3., 3., 3., 3.],
  9. # [ 4., 4., 4., 4.],
  10. # [ 5., 5., 5., 5.],
  11. # [ 6., 6., 6., 6.],
  12. # [ 7., 7., 7., 7.]])

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

  1. arr[[4, 3, 0, 6]]
  2. # array([[ 4., 4., 4., 4.],
  3. # [ 3., 3., 3., 3.],
  4. # [ 0., 0., 0., 0.],
  5. # [ 6., 6., 6., 6.]])

使用負數索引將從末尾開始選行:

  1. arr[[-3, -5, -7]]
  2. # array([[ 5., 5., 5., 5.],
  3. # [ 3., 3., 3., 3.],
  4. # [ 1., 1., 1., 1.]])

一次摻入多個索引數組會有一點特別。其返回的是一個一維數組,其中的元素對應各個索引元組:

  1. arr = np.arange(32).reshape((8, 4))
  2. arr
  3. # array([[ 0, 1, 2, 3],
  4. # [ 4, 5, 6, 7],
  5. # [ 8, 9, 10, 11],
  6. # [12, 13, 14, 15],
  7. # [16, 17, 18, 19],
  8. # [20, 21, 22, 23],
  9. 3 [24, 25, 26, 27],
  10. # [28, 29, 30, 31]])
  11. arr[[1, 5, 7, 2], [0, 3, 1, 2]]
  12. # array([ 4, 23, 29, 10])

以看到[ 4, 23, 29, 10]分別對應(1, 0), (5, 3), (7, 1), (2, 2)。不論數組有多少維,花式索引的結果總是一維。選取矩陣的行列子集可以使用如下方式:

  1. arr[[1, 5, 7, 2]][:, [0, 3, 1, 2]]
  2. # array([[ 4, 7, 5, 6],
  3. # [20, 23, 21, 22],
  4. # [28, 31, 29, 30],
  5. # [ 8, 11, 9, 10]])

上面的意思是,先從arr中選出[1, 5, 7, 2]這四行:

  1. array([[ 4, 5, 6, 7],
  2. [20, 21, 22, 23],
  3. [28, 29, 30, 31],
  4. [ 8, 9, 10, 11]])

然後[:, [0, 3, 1, 2]]表示選中所有行,但是列的順序要按0,3,1,2來排。於是得到:

  1. array([[ 4, 7, 5, 6],
  2. [20, 23, 21, 22],
  3. [28, 31, 29, 30],
  4. [ 8, 11, 9, 10]])

花式索引跟切片不同,總是將數據複製到新數組中。

數組轉置和軸對換

轉置是重塑的一種特殊形式,其返回源數據的視圖(不會進行任何複製操作)。有兩種方式,一是 transpose方法,二是 T屬性。

  1. arr = np.arange(15).reshape((3, 5))
  2. arr
  3. # array([[ 0, 1, 2, 3, 4],
  4. # [ 5, 6, 7, 8, 9],
  5. # [10, 11, 12, 13, 14]])
  6. arr.T
  7. # array([[ 0, 5, 10],
  8. # [ 1, 6, 11],
  9. # [ 2, 7, 12],
  10. # [ 3, 8, 13],
  11. # [ 4, 9, 14]])

再進行矩陣計算時,常需要該操作,如利用 np.dot計算內積:

  1. arr = np.random.randn(6,3)
  2. np.dot(arr.T,arr)
  3. # array([[ 1.8717599 , -1.66444711, -0.65044072],
  4. # [-1.66444711, 6.02759713, 0.05453921],
  5. # [-0.65044072, 0.05453921, 3.65394036]])

對於高維數組, transpose需要得到一個由軸編號組成的元組才能對這些軸進行轉置(比較費查克拉):

  1. arr = np.arange(16).reshape((2, 2, 4))
  2. arr
  3. # array([[[ 0, 1, 2, 3],
  4. # [ 4, 5, 6, 7]],
  5. # [[ 8, 9, 10, 11],
  6. # [12, 13, 14, 15]]])
  7. arr.transpose((1, 0, 2))
  8. # array([[[ 0, 1, 2, 3],
  9. # [ 8, 9, 10, 11]],
  10. # [[ 4, 5, 6, 7],
  11. # [12, 13, 14, 15]]])

其實就是把原本的軸按元組裡的內容重排一下。簡單的轉置可以用 .T。ndarray還有一個 swapaxes方法,需要接受一對軸編號:

  1. arr
  2. # array([[[ 0, 1, 2, 3],
  3. # [ 4, 5, 6, 7]],
  4. # [[ 8, 9, 10, 11],
  5. # [12, 13, 14, 15]]])
  6. arr.swapaxes(1, 2)
  7. # array([[[ 0, 4],
  8. # [ 1, 5],
  9. # [ 2, 6],
  10. # [ 3, 7]],
  11. # [[ 8, 12],
  12. # [ 9, 13],
  13. # [10, 14],
  14. # [11, 15]]])

swapaxes也是返回數據的視圖(不會進行任何複製操作)。

總結一下,只有花式索引和布爾型索引才會涉及到複製操作,其他的都是返回源數據的視圖。

推薦閱讀:

Python · 進度條
關於python遞歸的邏輯困惑?
Python Web學習路線圖
想擴展知識,學一門新語言,該學 Python、Ruby,還是 C++ ?

TAG:Python | numpy | 科学计算 |