Python可以這樣玩(10):分析與科學計算


學習完基本的GUI程式與繪圖功能之後,我們更進一步的來學習數學運算,並且將數學運算的結果用圖形來表示出來。很多人聽到數學就害怕,在這裡真的不需要,相反的,Python 反而可以幫你解決數學問題,絕對可以相輔相成。

是樂於資料分析與科學計算視覺化的模組相當多,我們就從一定要會的幾個模組開始。

擴展庫 numpy

如果你還沒有安裝 numpy 擴展庫,請透過 pip3 insatll numpy 安裝,這是一個基本的擴展庫,很多其他的擴展庫在安裝之前都需要先安裝它,所以應該不會有什麼問題。

在數學裡面有一樣東西叫做矩陣,電腦上稱為陣列,Python 的基本庫所提供的列表雖然很像陣列,但是並沒有計算功能。Numpy 提供了陣列,可以讓我們完成數學上的矩陣運算。

先看一下矩陣在數學上的定義,把行與列弄清楚,電腦也是一樣的定義:




矩陣的基本名詞:
(a)    (element):矩陣中列出來的每個數稱為矩陣的元。
(b)   (row):同一水平線各元合稱此矩陣的一列。
(c)    (column):同一鉛直線各元合稱此矩陣的一行。
(d)   位於第 i 列,第 j 行的元稱為(i,j)元。
(e)    當一個矩陣 M n m 行時,我們稱 M n×m 階的矩陣。
(f)     當一個矩陣Mnn行時,我們稱Mn階方陣。

產生陣列(Array)

電腦語言對矩陣的行、列定義都一樣,只有一點,就是數學的 (i,j) 1開始,Python [i][j] 0開始,關於這點到後面的運算便可以體會。下面直接看範例:

>>> import numpy as np
>>> np.array((1,2,3,4,5))
array([1, 2, 3, 4, 5])
>>> np.array([1,2,3,4,5]) # 元組或列表都可以轉成 array
array([1, 2, 3, 4, 5])
>>> np.array(range(1,6))
array([1, 2, 3, 4, 5])
>>> np.array([[1,2,3],[4,5,6]])
array([[1, 2, 3],
       [4, 5, 6]])
>>> np.linspace(0,10,11)  # 0 10 之間產生十個等差數列
array([ 0.,  1.,  2.,  3.,  4.,  5.,  6.,  7.,  8.,  9., 10.])
>>> np.linspace(0,8,11)
array([0. , 0.8, 1.6, 2.4, 3.2, 4. , 4.8, 5.6, 6.4, 7.2, 8. ])
>>> np.logspace(0,100,10) # 對數數列
array([1.00000000e+000, 1.29154967e+011, 1.66810054e+022, 2.15443469e+033,
       2.78255940e+044, 3.59381366e+055, 4.64158883e+066, 5.99484250e+077,
       7.74263683e+088, 1.00000000e+100])
>>> np.zeros((3,4))       # 有三列,四行,同數學定義
array([[0., 0., 0., 0.],
       [0., 0., 0., 0.],
       [0., 0., 0., 0.]])
>>> np.ones((4,3))
array([[1., 1., 1.],
       [1., 1., 1.],
       [1., 1., 1.],
       [1., 1., 1.]])
>>> np.identity(3)        # 單位距離
array([[1., 0., 0.],
       [0., 1., 0.],
       [0., 0., 1.]])
>>> np.empty((2,5))       # 空陣列,只給空間不初始化,元素值不確定
array([[1.00000000e+000, 1.29154967e+011, 1.66810054e+022,
        2.15443469e+033, 2.78255940e+044],
       [3.59381366e+055, 4.64158883e+066, 5.99484250e+077,
        7.74263683e+088, 1.00000000e+100]])
>>> np.empty((2,3))
array([[0., 0., 0.],
       [0., 0., 0.]])
>>> 

陣列與數值的演算

先看下面的數學定義:





>>> import numpy as np
>>> A = np.array([[1,2,3],[4,5,6]])
>>> A
array([[1, 2, 3],
       [4, 5, 6]])
>>> A * 2
array([[ 2,  4,  6],
       [ 8, 10, 12]])
>>> A * (-1)
array([[-1, -2, -3],
       [-4, -5, -6]])
>>> A / 2
array([[0.5, 1. , 1.5],
       [2. , 2.5, 3. ]])
>>> A // 2
array([[0, 1, 1],
       [2, 2, 3]], dtype=int32)
>>> A + 2
array([[3, 4, 5],
       [6, 7, 8]])
>>> A ** 2
array([[ 1,  4,  9],
       [16, 25, 36]], dtype=int32)
>>> A % 3
array([[1, 2, 0],
       [1, 2, 0]], dtype=int32)
>>> 

關於數值與矩陣的運算,數學上其實只有定義 rA,並沒有定義 r + A,所以前面出現的加法減法,單純屬於 Python 運算子功能的範疇,只能用電腦程式語言的觀點來看,不能用數學來思考。

陣列與陣列的運算




數學上定義的矩陣加減法,與Python 相同,沒有問題:

>>> A = np.array([[9,4,0],[-5,6,7]])
>>> B = np.array([[-1,2,3],[4,5,6]])
>>> A + B             # 與上面答案相同
array([[ 8,  6,  3],
       [-1, 11, 13]])
>>> A - B
array([[10,  2, -3],
       [-9,  1,  1]])
>>> C = A * B         # 數學上乘法的定義並非如此
>>> C
array([[ -9,   8,   0],
       [-20,  30,  42]])
>>> C / A             # 分母為0 報錯

Warning (from warnings module):
  File "__main__", line 1
RuntimeWarning: invalid value encountered in true_divide
array([[-1.,  2., nan],
       [ 4.,  5.,  6.]])
>>> C / B
array([[ 9.,  4.,  0.],
       [-5.,  6.,  7.]])
>>> B * B
array([[ 1,  4,  9],
       [16, 25, 36]])
>>> B / B
array([[1., 1., 1.],
       [1., 1., 1.]])
>>> B - B
array([[0, 0, 0],
       [0, 0, 0]])

Python * 運算子作用在兩個 array 上面的時候,就是很單純的把兩陣列每個元素相乘,這在數學上並不叫做矩陣相乘,而是叫做 阿達馬乘積,定義如下:


>>> x = np.array([[1,3,2],[1,0,0,],[1,2,2]])
>>> x = np.array([[1,3,2],[1,0,0],[1,2,2]])
>>> x
array([[1, 3, 2],
       [1, 0, 0],
       [1, 2, 2]])
>>> y = np.array([[0,0,2],[7,5,0],[2,1,1]])
>>> y
array([[0, 0, 2],
       [7, 5, 0],
       [2, 1, 1]])
>>> x * y
array([[0, 0, 4],
       [7, 0, 0],
       [2, 2, 2]])
>>> 

但是真正的矩陣相乘的定義,稱為一般矩陣乘積,定義如下:


直接由定義計算:


這種矩陣乘積亦可由稍微不同的觀點來思考:把向量和各係數相乘後相加起來。設AB是兩個給定如下的矩陣:


舉個例子來說明:


>>> a = np.array([[1,0,2],[-1,3,1]])
>>> a
array([[ 1,  0,  2],
       [-1,  3,  1]])
>>> b = np.array([[3,1],[2,1],[1,0]])
>>> b
array([[3, 1],
       [2, 1],
       [1, 0]])
>>> a * b
Traceback (most recent call last):
  File "<pyshell#45>", line 1, in <module>
    a * b
ValueError: operands could not be broadcast together with shapes (2,3) (3,2)
>>> 

當然報錯啦,因為它們的維度不同,前面說過維度不同的兩個矩陣式不可以做加法運算的,這裡 array 的乘法比照矩陣加法運算原則。那麼數學上的矩陣相乘在 Python 要如何表呢?先別著急,我們後面對詳細說明。由於 array 與數學的矩陣有很多共通處,所以我利用數學觀念來談 array,順便讓讀者溫習一下數學,對後面的大數據分析會有幫助。

二維陣列轉置


上面的例子,因為維度不同,我們不能運算,一個是 3x2,一個是 2x3,如果們將其中一個 array 透過轉置,就可以運算了。所以,轉置的定義很簡單,就是將 3x2 變成 2x3。數學上的定義是這樣:




不過如果是一維陣列,轉置出來的結果是一樣的,其實這點與數學的矩陣是不同的,這點要記住:
>>> a = np.array([1,2,3,4,5])
>>> a
array([1, 2, 3, 4, 5])
>>> a.T
array([1, 2, 3, 4, 5])
>>> 

向量積

又稱為向量內積,還記得數學上矩陣的乘法運算嗎?寫成 A x B = C,乘積C中的每一個元素,就是A的列元素與B的行元素的向量積,也就是把元素相乘之後再加總。




>>> a = np.array([1,2,3])
>>> b = np.array([4,5,6])
>>> a.dot(b)
32
>>> np.dot(a,b)
32
>>> 

我們再看一下二維與一維的運算:
>>> c = np.array([[1,2,3],[4,5,6],[7,8,9]])
>>> c
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])
>>> a
array([1, 2, 3])
>>> c.dot(a)
array([14, 32, 50])
>>> ct = c.T
>>> ct
array([[1, 4, 7],
       [2, 5, 8],
       [3, 6, 9]])
>>> ct.dot(a)
array([30, 36, 42])

有趣的事情來了,兩個一維陣列的內積就是兩個向量的內積會等於一個數字,但是兩個多維陣列的內積則會是一個多維陣列,這點跟前面談到的數學上矩陣乘積有點類似,所以我們再看前面的例子:

>>> a = np.array([[1,0,2],[-1,3,1]])
>>> b = np.array([[3,1],[2,1],[1,0]])
>>> a
array([[ 1,  0,  2],
       [-1,  3,  1]])
>>> b
array([[3, 1],
       [2, 1],
       [1, 0]])
>>> a.dot(b)
array([[5, 1],
       [4, 2]])

答案就對了,所以說,我們可以下個結論,Python 的陣列A * 陣列B,其結果是數學矩陣的阿達馬乘積;Python 的陣列A.dot(陣列B)、或是 dot(陣列A, 陣列B)才是數學矩陣的乘積。

特別延伸說明,A * B = B * A,乘法有交換率,但是向量內積的交換率不存在:




不過只有一個例外,就是當 A B 其中一個為單位方陣的時候才成立。請參考數學範例:




什麼是單位方陣呢?就是一開始我們提到的 np.identity(3),數學定義如下:




看完這樣的說明,相信對 identity() 的用處就更清楚了:
>>> a
array([[ 1,  0,  2],
       [-1,  3,  1]])
>>> b
array([[3, 1],
       [2, 1],
       [1, 0]])
>>> a.dot(b)
array([[5, 1],
       [4, 2]])
>>> b.dot(a)
array([[2, 3, 7],
       [1, 3, 5],
       [1, 0, 2]])
>>> c=b.dot(a)
>>> c
array([[2, 3, 7],
       [1, 3, 5],
       [1, 0, 2]])
>>> d = np.identity(3)
>>> d
array([[1., 0., 0.],
       [0., 1., 0.],
       [0., 0., 1.]])
>>> c.dot(d)
array([[2., 3., 7.],
       [1., 3., 5.],
       [1., 0., 2.]])
>>> d.dot(c)
array([[2., 3., 7.],
       [1., 3., 5.],
       [1., 0., 2.]])
>>> 

存取陣列元素

數學的部分談完了,接下來就是 Python 語法部分,如何透過 index 取出陣列的元素呢?

>>> c = np.array([[1,2,3],[4,5,6],[7,8,9]])
>>> c
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])
>>> c[0]
array([1, 2, 3])
>>> c[0][1]
2
>>> 

陣列元素還支援多個元素同時存取:
>>> x = np.linspace(0,100,11)
>>> x
array([  0.,  10.,  20.,  30.,  40.,  50.,  60.,  70.,  80.,  90., 100.])
>>> index = np.random.randint(0, len(x), 5)
>>> index
array([ 9, 10,  4,  9,  7])
>>> x[index]
array([ 90., 100.,  40.,  90.,  70.])
>>> 

對陣列進行函數運算

np.arange() 的作用就跟之前談過去 range() 類似:
>>> x = np.arange(0,100,10)
>>> x
array([ 0, 10, 20, 30, 40, 50, 60, 70, 80, 90])
>>> np.sin(x)
array([ 0.        , -0.54402111,  0.91294525, -0.98803162,  0.74511316,
       -0.26237485, -0.30481062,  0.77389068, -0.99388865,  0.89399666])
>>> x = np.arange(0,100,9)
>>> x
array([ 0,  9, 18, 27, 36, 45, 54, 63, 72, 81, 90, 99])

對陣列不同維度上的元素進行運算

直接看例子:
>>> x = np.arange(0,10).reshape(2,5)
>>> x
array([[0, 1, 2, 3, 4],
       [5, 6, 7, 8, 9]])
>>> np.sum(x)
45
>>> np.sum(x, axis=0)
array([ 5,  7,  9, 11, 13])
>>> np.sum(x, axis=1)
array([10, 35])
>>> 

改變陣列大小

>>> a = np.arange(1,11,1)
>>> a
array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10])
>>> a.reshape(2,5)
array([[ 1,  2,  3,  4,  5],
       [ 6,  7,  8,  9, 10]])
>>> a
array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10])
>>> a.shape = 2,5
>>> a
array([[ 1,  2,  3,  4,  5],
       [ 6,  7,  8,  9, 10]])

剛才一維陣列無法轉置問題,可以這樣解決,就是用 shape 先轉置一次,以後就可以做一維的轉置了,不同處在紅色處:
>>> a = np.arange(1,11,1)
>>> a
array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10])
>>> a.shape = 10,1
>>> a
array([[ 1],
       [ 2],
       [ 3],
       [ 4],
       [ 5],
       [ 6],
       [ 7],
       [ 8],
       [ 9],
       [10]])
>>> a.T
array([[ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10]])
>>> b = a.T
>>> b
array([[ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10]])
>>> b.T
array([[ 1],
       [ 2],
       [ 3],
       [ 4],
       [ 5],
       [ 6],
       [ 7],
       [ 8],
       [ 9],
       [10]])
>>> 

這種雙括號的一列陣列,其實就是 Python 的矩陣了,我們來比較一下下面三種的不同:

>>> x = np.matrix(range(1,11,1))
>>> x
matrix([[ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10]])
>>> y = np.arange(1,11,1)
>>> y
array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10])
>>> z = list(range(1,11,1))
>>> z
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> x.T
matrix([[ 1],
        [ 2],
        [ 3],
        [ 4],
        [ 5],
        [ 6],
        [ 7],
        [ 8],
        [ 9],
        [10]])
>>> y.T
array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10])
>>> z.T
Traceback (most recent call last):
  File "<pyshell#141>", line 1, in <module>
    z.T
AttributeError: 'list' object has no attribute 'T'
>>> y
array([ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10])
>>> y.shape = 10,1
>>> y
array([[ 1],
       [ 2],
       [ 3],
       [ 4],
       [ 5],
       [ 6],
       [ 7],
       [ 8],
       [ 9],
       [10]])
>>> y = y.T
>>> y
array([[ 1,  2,  3,  4,  5,  6,  7,  8,  9, 10]])
>>> 


科學計算擴展庫 scipy

透過 pip install scipy 就可以安裝此擴展庫,此擴展庫在 numpy 的基礎上面增加大量應用於科學計算與工程計算的模組。SciPy包含的模組有最佳化線性代數積分插值特殊函式快速傅立葉變換訊號處理圖像處理常微分方程求解和其他科學與工程中常用的計算。

由於這個擴展庫太過於複雜,因為科學的領域太大,我就只針對最基本的淺談,其他深入的部分,就搭配像是 matplotlib 擴展庫使用的時候,再行討論。

數學、物理常用常數與單位模組 constants

例如,下面的方法可以用來存取預先定義的常數:
>>> from scipy import constants as C
>>> C.pi
3.141592653589793
>>> C.golden
1.618033988749895
>>> C.c
299792458.0
>>> C.mile
1609.3439999999998
>>> C.inch
0.0254
>>> C.oz
0.028349523124999998
>>> C.lb
0.45359236999999997

特殊函數模組

一些特別的數學函數,包含計算立方根,排列組合等。
>>> from scipy import special as S
>>> S.cbrt(8)
2.0
>>> S.exp10(3)
1000.0
>>> S.sindg(90)
1.0
>>> S.round(3.5)
4.0
>>> S.round(3.4)
3.0
>>> S.comb(5,3)
10.0
>>> S.perm(5,3)
60.0

留言

這個網誌中的熱門文章

Python可以這樣玩(16):共陰/共陽七段顯示器

Python可以這樣玩(11):數學繪圖

Python可以這樣玩(15):蜂鳴器與音樂