博客 / 詳情

返回

利用 Python 進行數據分析 —— 2 NumPy 基礎

什麼是 NumPy?根據其官方文檔的介紹:

NumPy 是Python中科學計算的基礎包。它是一個Python庫,提供多維數組對象,各種派生對象(如掩碼數組和矩陣),以及用於數組快速操作的各種API,有包括數學、邏輯、形狀操作、排序、選擇、輸入輸出、離散傅立葉變換、基本線性代數,基本統計運算和隨機模擬等等。

NumPy 的核心是一個特殊的數組對象——ndarray 對象。當運算涉及到 ndarray 對象時,默認通過預編譯的 C 代碼對逐個元素操作,在運算大量數據時,運算速度極快。此外,我們還可以看到 NumPy 的語法更簡單。

乍一看有些籠統。那麼,就讓我們一點一點,揭開 NumPy 的面紗。

2.1 ndarray: 一種多維數組對象

想使用 NumPy,首先讓我們導入 NumPy 包:

import numpy as np

2.1.1 創建 ndarray 對象

NumPy 的所有運算幾乎都是圍繞數組( ndarray 對象)展開的。那麼如何創建數組呢?

簡單的,我們可以將一個序列轉換為數組。

in : data1 = [6, 7.5, 8, 0, 1]
     arr1 = np.array(data1)  # np.array() 可以將一切序列型的對象轉換為一個數組
     arr1
out: array([6. , 7.5, 8. , 0. , 1. ])

in : data2 = [[1, 2, 3, 4], [5, 6, 7, 8]]
     arr2 = np.array(data2)  # 等長嵌套序列將被轉換為多維數組
     arr2
out: array([[1, 2, 3, 4],
            [5, 6, 7, 8]])

也有其他的函數能幫助我們直接創建一些特殊數組。

in : np.zeros((2, 3))  # 創建指定規格的全 0 數組
out: array([[0., 0., 0.],
            [0., 0., 0.]])

in : np.ones(4)  # 創建指定規格的全 1 數組
out: array([1., 1., 1., 1.])

in : np.arange(10)  # 類似 range()
out: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

in : data = np.random.randn(2, 3)  # 創建指定規格的隨機值數組(基於標準正態分佈)
     data
out: array([[-0.22036948, -0.92073677,  0.38747356],
            [ 0.48489615, -0.74062949,  0.5220284 ]])

每個數組都有兩個基本屬性:shape (表示各維度大小的元組),dtype (用於説明數組數據類型的對象)。

in : data.shape
out: (2, 3)

in : data.dtype
out: dtype('float64')

2.1.2 ndarray 的數據類型

NumPy 能自動為輸入的序列設定一個合適的數據類型。

in : arr1 = np.array([1, 2, 3])
     arr1.dtype
out: dtype('int32')

in : arr2 = np.array([1.2, 0.78, 5])
     arr2.dtype
out: dtype('float64')

也可以自己手動設置數組的數據類型。

arr3 = np.array([1, 2, 3], dtype=np.float64)  # 注意這裏的類型名稱是“np.float64”

如果你想對一個數組進行類型轉換,可以使用 astype() 。但是注意,astype() 並非在原數組上進行修改,而是會建立一個新數組。

in : arr3.astype(np.int32)
out: array([1, 2, 3])

in : arr3.dtype
out: dtype('float64')  # 可以看到,原數組 arr3 的數據類型並未被修改

2.1.3 相同大小數組的運算

大小相等的數組之間,任何算術運算都會將運算應用到元素級。

in : arr = np.array([[1., 2., 3.], [4., 5., 6.]])
     arr
out: array([[1., 2., 3.],
            [4., 5., 6.]])

in : arr * arr
out: array([[ 1.,  4.,  9.],
            [16., 25., 36.]])

in : arr + arr
out: array([[ 2.,  4.,  6.],
            [ 8., 10., 12.]])

in : arr ** 2  # 冪運算
out: array([[ 1.,  4.,  9.],
            [16., 25., 36.]])

數組之間進行比較,會生成布爾值數組。

in : arr2 = np.array([[0., 4., 1.], [7., 2., 12.]])
     arr2
out: array([[ 0.,  4.,  1.],
            [ 7.,  2., 12.]])

in : arr2 > arr
out: array([[False,  True, False],
            [ True, False,  True]])

2.1.4 基本的索引和切片

我們先來看一維數組。首先建立一個一維數組:

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

創建數組切片的方式與創建序列切片的方式相同。

in : arr[5:8]
out: array([5, 6, 7])

對切片賦值會被應用到每個元素之上。

in : arr[5:8] = 12  # 等式右邊輸入 1 個值,即可對切片包含的所有值賦值
     arr
out: array([ 0,  1,  2,  3,  4, 12, 12, 12,  8,  9])

in : arr = 12  # 這樣賦值的話,arr 就被賦值為一個整數了,並不能保持原來的數組結構
     arr
out: 12

# 如果想在原來的數組結構上,將所有值賦值為一個整數,可以這樣:
in : arr[:] = 12  
     arr
out: array([12, 12, 12, 12, 12, 12, 12, 12, 12, 12])

即使將切片保存為一個新的數組,對新數組的任何修改也會被應用到原數組上。這是因為 NumPy 為了處理大量數據,需要避免複製數組造成的佔用運算性能問題。可以説,我們並沒得到原數組切片的一個副本,而是得到了原數組切片的一個視圖。

in : arr_slice = arr[5:8]  # 創建一個數組切片的視圖
     arr_slice[0] = 100
     arr
out: array([ 12,  12,  12,  12,  12, 100,  12,  12,  12,  12])  # 可以看到原數組也被改變了

以上特性是和 Python 基礎的序列對象不同的。以 list 為例:

in : list1 = list(range(10))
     list1
out: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

in : list1[5:8] = 12  # 普通的 python 列表則不能直接以這樣的方式賦值給每個元素(NumPy 數組可以)
out: Traceback (most recent call last):
       File "<input>", line 1, in <module>
     TypeError: can only assign an iterable

in : list1[5:8] = [12, 12, 12]  # 可以通過這樣的方法賦值
     list1
out: [0, 1, 2, 3, 4, 12, 12, 12, 8, 9]

in : list1_slice = list1[5:8]
     list1_slice[0] = 100
     list1
out: [0, 1, 2, 3, 4, 12, 12, 12, 8, 9]  # 可以看到原列表沒有被改變

如果想得到數組切片的副本(而不是視圖),可以採用 copy() 。

in : arr_slice_copy = arr[5:8].copy()  # 創建一個數組切片的副本
     arr_slice_copy[0] = 123456
     arr
out: array([ 12,  12,  12,  12,  12, 100,  12,  12,  12,  12])  # 可以看到原數組就沒有被改變了

那麼如何對二維數組進行切片呢?

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

in : arr2d[0, 2]
     arr2d[0][2]  # 這兩種索引方式是一樣的效果
out: 3

in : arr2d[:2]  # 對第0軸切片
out: array([[1, 2, 3],
            [4, 5, 6]])  # 切片後,數組仍是個二維數組

in : arr2d[:2, :1]  # 對第0軸、第1軸切片
out: array([[1],
            [4]])  # 切片後,數組仍是個二維數組

# 注:可以看到,切片不改變數組的維度

2.1.5 布爾型索引

我們先建立兩個數組用於舉例:

in : names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
     data = np.random.randn(7, 4)
     names
     data
out: array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'], dtype='<U4')
     array([[ 0.05817461,  0.82783485,  0.46839737,  0.50975782],
            [ 1.15212531,  1.4481847 , -0.4201631 , -0.30265817],
            [ 1.05658084, -0.38813849,  0.16464375,  1.48137849],
            [-0.64680744, -0.90829029, -0.74018895,  0.17760956],
            [ 1.96177662,  0.747304  ,  0.08973106,  1.37109694],
            [-0.46767001, -0.50060317,  0.07396933,  0.8385746 ],
            [-1.71710491, -0.58035244,  2.19566878,  1.33896025]])

邏輯表達式可以用於生成布爾型數組,這個布爾型數組可以用於索引。

in : names == 'Bob'  # 用邏輯表達式生成布爾型數組
out: array([ True, False, False,  True, False, False, False])

in : data_bob = data[names == 'Bob']  # 布爾型數組用於索引
                                   # 注意:布爾型數組的長度必須跟被索引的軸長度一致
     data_bob
out: array([[ 0.05817461,  0.82783485,  0.46839737,  0.50975782],
            [-0.64680744, -0.90829029, -0.74018895,  0.17760956]])

布爾型數組用於索引時,可以用 “~” 反轉條件。

in : data[~(names == 'Bob')]  #  ~ 用於反轉條件
out: array([[ 1.15212531,  1.4481847 , -0.4201631 , -0.30265817],
            [ 1.05658084, -0.38813849,  0.16464375,  1.48137849],
            [ 1.96177662,  0.747304  ,  0.08973106,  1.37109694],
            [-0.46767001, -0.50060317,  0.07396933,  0.8385746 ],
            [-1.71710491, -0.58035244,  2.19566878,  1.33896025]])

創建布爾型索引時,Python 能識別的 not, and, or 是無效的,需要使用 ! & | 。

in : data[(names != 'Bob') & (names != 'Joe')]
out: array([[ 1.05658084, -0.38813849,  0.16464375,  1.48137849],
            [ 1.96177662,  0.747304  ,  0.08973106,  1.37109694]])

2.1.6 數組的結構重塑和轉置

如果想把一個一維數組進行結構重塑,變成 3 × 5 的二維數組,該怎麼做呢?

in : arr = np.arange(15).reshape((3, 5))  # reshape() 用於數組的結構重塑
     arr
out: array([[ 0,  1,  2,  3,  4],
            [ 5,  6,  7,  8,  9],
            [10, 11, 12, 13, 14]])

如果想把上述數組轉置,該怎麼做呢?

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

2.2 通用函數(ufunc)

通用函數( ufunc )是一種對 ndarray 中的數據執行元素級運算的函數。

ufunc 需要至少一個數組作為參數。輸入一個數組的 ufunc,被稱為一元 ufunc。

in : arr = np. arrange(10)
     arr
out: array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

# 開平方
in : np.sqrt(arr)
out: array([0.        , 1.        , 1.41421356, 1.73205081, 2.        ,
            2.23606798, 2.44948974, 2.64575131, 2.82842712, 3.        ])

# 自然底數 e 的 x 次方
in : np.exp(arr)
out: array([1.00000000e+00, 2.71828183e+00, 7.38905610e+00, 2.00855369e+01,
            5.45981500e+01, 1.48413159e+02, 4.03428793e+02, 1.09663316e+03,
            2.98095799e+03, 8.10308393e+03])

而輸入兩個數組的 ufunc 被稱為 二元 ufunc。

in : x = np.random.randn(8)
     y = np.random.randn(8)
     x
     y
out: array([ 0.88741632, -0.47989197,  0.961642  ,  1.43875204, -1.28796969,
            -0.04834001,  0.27405319, -0.03876604])
     array([-2.25585454,  0.9791547 ,  2.72284215, -0.44911582,  0.39418769,
            -0.73406601, -0.77432083, -3.21548859])

# 返回兩個數組中,對應位置更大的值組成的數組(與 x, y 相同結構)
in : np.maximum(x, y) 
out: array([ 0.88741632,  0.9791547 ,  2.72284215,  1.43875204,  0.39418769,
            -0.04834001,  0.27405319, -0.03876604])

也有能返回兩個結果數組的函數。

in : arr = np.random.randn(7) * 5
     arr
out: array([-7.01330576, -0.40563694,  5.65876183, -6.69774849,  0.42850616,
             4.65646863,  4.64433757])

# 返回浮點數數組的小數和整數部分
in : remainder, whole_part = np.modf(arr)
     remainder
     whole_part
out: array([-0.01330576, -0.40563694,  0.65876183, -0.69774849,  0.42850616,
             0.65646863,  0.64433757])
     array([-7., -0.,  5., -6.,  0.,  4.,  4.])  # 雖然是整數部分,但數組類型仍是“float64”

通用函數都並非在原始數組上進行更改,而是構建了一個新的數組。如果想原地操作數組,可以明確指明 out 參數。

in : arr = np.random.randn(5)
     arr
out: array([-0.97691849,  0.31726746, -0.75027946,  0.51031132, -0.54012687])

in : np.sqrt(arr)
     arr
out: array([-0.97691849,  0.31726746, -0.75027946,  0.51031132, -0.54012687])  # 可以看到原數組沒有被修改

in : np.sqrt(arr, arr)
out: array([       nan, 0.563265  ,        nan, 0.71436078,        nan])

其他的通用函數羅列於此:

一元 ufunc
二元 ufunc

2.3 利用數組進行數據處理

NumPy 數組可以讓我們在不編寫循環的情況下,用簡潔的數組表達式進行數學運算。這種用數組表達式代替循環的做法,通常被稱為矢量化。

比如,我們現在想對一個網格型數據計算:

$$ \sqrt{(x^2 + y^2)} $$

首先,讓我們創建一個網格型數據。

in : points1 = np.arange(-5, 0)  # x軸
     points2 = np.arange(0, 5)  # y軸
     points1
     points2
out: array([-5, -4, -3, -2, -1])
     array([0, 1, 2, 3, 4])

# np.meshgrid() 可以接受兩個一維數組,併產生兩個二維矩陣。
# 可以把上面建立的網格畫出來,將x軸和y軸組成的網格中,每個 (x, y) 都寫出來,就容易理解了。
# xs 是這個網格上 (x, y) 中 x 組成的二維矩陣;ys 是這個網格上 (x, y) 中 y 組成的二維矩陣;
in : xs, ys = np.meshgrid(points1, points2)
     xs
     ys
out: array([[-5, -4, -3, -2, -1],
            [-5, -4, -3, -2, -1],
            [-5, -4, -3, -2, -1],
            [-5, -4, -3, -2, -1],
            [-5, -4, -3, -2, -1]])
     array([[0, 0, 0, 0, 0],
            [1, 1, 1, 1, 1],
            [2, 2, 2, 2, 2],
            [3, 3, 3, 3, 3],
            [4, 4, 4, 4, 4]])

# 現在,我們就可以來計算上面的公式了
in : z = np.sqrt(xs ** 2 + ys ** 2)  # NumPy 讓我們可以像計算浮點數一樣,編寫計算數組的代碼
     z
out: array([[5.        , 4.        , 3.        , 2.        , 1.        ],
            [5.09901951, 4.12310563, 3.16227766, 2.23606798, 1.41421356],
            [5.38516481, 4.47213595, 3.60555128, 2.82842712, 2.23606798],
            [5.83095189, 5.        , 4.24264069, 3.60555128, 3.16227766],
            [6.40312424, 5.65685425, 5.        , 4.47213595, 4.12310563]])

2.3.1 將條件邏輯表述為數組運算

這一節介紹一下 np.where() ,它是三元表達式 x if condition else y 的矢量化。

假設我們有一個布爾數組和兩個值數組。

xarr = np.array([1.1, 1.2, 1.3, 1.4, 1.5])
yarr = np.array([2.1, 2.2, 2.3, 2.4, 2.5])
cond = np.array([True, False, True, True, False])

我們想要根據 cond 中的值選取 xarr 和 yarr 的值:當 cond 中的值為 True 時,選取 xarr 的值,否則從 yarr 中選取。

如果用列表推導式編寫:

result = [(x if c else y) for x, y, c in zip(xarr, yarr, cond)]

但如果用 np.where() 編寫,代碼會簡潔很多:

in : result = np.where(cond, xarr, yarr)
     result
out: array([1.1, 2.2, 1.3, 1.4, 2.5])

np.where() 的第一個參數是邏輯型數組,第二、三個參數則無需是數組。該函數的邏輯是:根據第一個邏輯型數組參數進行判定,若是,把數據替換為參數2,;若不是,把數據替換為參數3。

比如,我們想把一個二維數組中,正數賦值為2,負數保持不變。

in : arr = np.random.randn(4,4)
     arr
out: array([[-1.14810005, -1.26020793, -0.88391974,  0.92820162],
            [-0.37743466,  0.72906491, -2.08884266,  1.02572131],
            [-0.18377037,  0.92768921,  0.80025967,  1.63431864],
            [ 2.05365743, -0.13690236, -0.84462678,  1.96340251]])
     
in : result = np.where(arr > 0, 2, arr)
out: array([[-1.14810005, -1.26020793, -0.88391974,  2.        ],
            [-0.37743466,  2.        , -2.08884266,  2.        ],
            [-0.18377037,  2.        ,  2.        ,  2.        ],
            [ 2.        , -0.13690236, -0.84462678,  2.        ]])

2.3.2 數學和統計方法

NumPy 可以對整個數組進行數學統計,舉例如下:

in : arr = np.random.randn(3,2)
     arr
out: array([[ 0.39769436, -0.8714971 ],
            [-1.98139244, -0.91648828],
            [-0.61709608,  0.82630001]])

in : arr.mean()  # 求均值
     np.mean(arr)  # 和上式是等價的
out: -0.527079921854901

in : arr.std()  # 求標準差
out: 0.9201661620576673

in : arr.sum()  # 求和
out: -3.1624795311294065

in : arr.mean(axis=0)  # 計算每列的均值
out: array([-0.73359805, -0.32056179])

in : arr.sum(axis=1)  # 計算每行的和
out: array([-0.47380273, -2.89788073,  0.20920393])

2.3.3 用於布爾型數組的方法

上一節的方法也可以應用到布爾型數組裏,此時 True = 1,False = 0。

in : arr = np.random.randn(100)
     (arr > 0).sum()  # 可以利用 sum() 對布爾型數組中的 True 值計數
out: 52

對於布爾型數組而言,也有一些特殊的常用方法。

in : bools = np.array([False, False, True, False])
     bools.any()  # 數組中是否存在一個或多個 True
out: True

in : bools.all()  # 數組中是否都是 True
out: False

2.3.4 排序

如何對數組中的值排序呢?

in : arr = np.random.randn(6)
     arr
out: array([-0.30308892, -1.18380195, -1.06029921, -0.92172841, -0.25920488, -0.53782583])

# 將數組原地排序
in : arr.sort()  
     arr
out: array([-1.18380195, -1.06029921, -0.92172841, -0.53782583, -0.30308892, -0.25920488])

對於多維數組來説,指明 sort() 中的軸參數,可以按軸對數組進行排序。

in : arr = np.array([[1, 6, 2], [8, 2, 0], [5, 7, 9]])
     arr
out: array([[1, 6, 2],
            [8, 2, 0],
            [5, 7, 9]])

in : arr.sort(1)
     arr
out: array([[1, 2, 6],
            [0, 2, 8],
            [5, 7, 9]])

in : arr.sort(0)
     arr
out: array([[0, 2, 6],
            [1, 2, 8],
            [5, 7, 9]])

2.3.5 唯一化和其他的集合邏輯

在數據分析中,常常需要從一個包含重複值的數組中,提取出唯一值。這時,我們可以使用 np.unique()。

in : names = np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
     np.unique(names)  # 返回不包含重複值、已排序的結果數組
out: array(['Bob', 'Joe', 'Will'], dtype='<U4')

in : ints = np.array([3, 3, 3, 2, 2, 1, 1, 4, 4])
     np.unique(ints)
out: array([1, 2, 3, 4])

有時,還需要判斷一個數組中的值,是否包含在另一個數組中。

in : values = np.array([6, 0, 0, 3, 2, 5, 6])
     np.in1d(values, [2, 3, 6])  # 返回一個布爾型結果數組
out: array([ True, False, False,  True,  True, False,  True])

2.4 偽隨機數生成

本節以上的部分,我們使用了很多次 np.random.randn() 函數來創建一個隨機數構建的數組,它表示按照標準正態分佈取隨機值。NumPy 生成的隨機數並非真正的隨機數,而是根據一個隨機種子通過算法計算得到的,被稱為偽隨機數。如果你願意,也可以更改隨機種子。

其他的函數可以用於生成不同的偽隨機數數組:

np.random()

2.5 示例:隨機漫步

學習了很多零散的知識,接下來我們來看一個綜合運用的例子。

假設我們的初始位置是 0,往前走一步記為 1,往後走一步記為 -1。我們開始隨機漫步,即步長為 1 和 -1 出現的概率相等,每次我們都可以往前走一步,或者往後走一步,一共走 1000 步。

nsteps = 1000
draws = np.random.randint(0, 2, size=nsteps)  # np.random.randint() 是從給定上下限的範圍內隨機選取整數
steps = np.where(draws > 0, 1, -1)
walk = steps.cumsum()  # 生成漫步路徑:累加,返回每一步的累加值構成的結果數組

接下來,讓我們沿着漫步路徑做一些統計工作。

往後最遠走到哪裏?

walk.min()

往前最遠走到哪裏?

walk.max()

第一次走到第 10 步遠是什麼時候?

for i, walk_len in enumerate(np.abs(walk) >= 10):
    if walk_len == True:
        print(i)
        break

# 或者,用下面的方式更簡潔:
(np.abs(walk) >= 10).argmax()  # argmax() 返回第一個最大值的索引(True 就是布爾型數組中的最大值)
注:轉載請註明出處。

本文屬於《利用 Python 進行數據分析》讀書筆記系列:

  • 利用 Python 進行數據分析 —— 1 數據結構、函數和文件
user avatar u_16213589 頭像 kaige 頭像 hellolvs 頭像 backofhan 頭像 starrocks 頭像
5 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.