2019年1月31日 星期四

Python 學習筆記 : 陣列模組 array 測試

本篇探討 Python 的串列與 array 模組的功能在資料科學方面的限制, 作為學習 Numpy 的前置測試. 本篇測試參考了如下書籍 (特別是第 6 本) :
  1. Python 資料科學手冊 (碁峰, 何敏煌譯)
  2. 高效率資料分析-使用 Python (碁峰, 賴屹民譯)
  3. Python 資料運算與分析實戰 (旗標, 莊永裕譯)
  4. Python 資料科學與人工智慧應用實務 (旗標, 陳允傑)
  5. Python程式設計學習經典:工程分析x資料處理x專案開發 (碁峰, 吳翌禎, 黃立政)
  6. Data Science from Scratch 中文版 : 用 Python 學資料科學 (碁峰, 籃子軒譯)
資料科學中的數學運算大部分是工程數學中的線性代數, 主要是向量與矩陣運算, 所謂向量 (vector) 是相對於純量 (scaler) 而言的概念, 在物理學中純量是只有大小沒有方向的實數, 而向量則是有大小也有方向的量, 例如溫度, 體積, 質量, 電量, 速率等等都是純量, 而速度, 位移, 磁矩等等則是向量, 參考 :

https://zh.wikipedia.org/wiki/标量 (純量)
https://zh.wikipedia.org/wiki/向量 (矢量, 向量的別名)

但是在線性代數中的向量指的是組成向量空間 (例如一個線性坐標系) 的元素, 一個 n 維向量是以一個有序純量數列例如 (a1, a2, a3, .... an) 來表示 (這也是此向量的終點座標), 其中每一個元素代表向量在該維之分量. 而一個矩陣是二維的有序數列, 可以視為由多個列向量或行向量組成 (默認是列向量). 在程式語言中使用陣列來表示向量 (一維) 或矩陣 (二維), 高於二維的陣列則稱為張量 (Tensor).

陣列是科學計算最常用的資料型態, 不過 Python 內建的資料型態 List  (串列) 不是陣列, 它是比陣列更具彈性的序列型資料結構, 它與其他語言如 Java 的陣列不同之處在於 :
  1. List 的元素可以是任何同質或異質資料 (可混合), 而 Java 的陣列必須是同質性資料. Python 的串列相當於 Java 中的 ArrayList 型態. 
  2. List 的長度是可變的 , 具有 append(), pop() 等函數; 但 Java 的陣列長度宣告後即固定不可變.
Python 從 3.3 版開始增加了一個內建模組 array 以支援陣列運算, 使用前須先引入 :

import array     

然後呼叫建構式 array() 並傳入一個串列以建立 array.array 物件 :

a=array.array('type code', list)    #傳入串列 list 將其轉成陣列

其中必要的第一參數 type code 是一個字元, 用來指定陣列元素之資料型態, 數值計算常用的type code 如下 :

  • 'i' : signed integer (2 bytes)
  • 'I' : unsigned integer (2 bytes)
  • 'l' : signed long (4 bytes)
  • 'L' : unsigned long (4 bytes)
  • 'f' : float (4 bytes)
  • 'd' : double (8 bytes)

參考 :

Python Arrays

陣列物件可用與 List 一樣的索引與 slice 方法存取元素, 例如 :

>>> import array
>>> a=array.array('i', [3, 6, 9, 12])
>>> type(a)
<class 'array.array'>
>>> print(a)
array('i', [3, 6, 9, 12])
>>> a[0]
3
>>> a[1]
6
>>> a[2]
9
>>> a[3]
12
>>> a[-1]    #負索引表示從最後面 (-1) 往前定位元素
12

array.array 物件提供如下方法以便操作陣列 :

 array.array 物件的方法 說明
 append(element) 將元素 element 添加到陣列尾端
 extend(list) 將串列 list 添加到陣列尾端
 remove(element) 將元素 element 從陣列中移除 (一次一個)
 pop(index) 將索引為 index 之元素從陣列中移除並傳回

例如 :

>>> a
array('i', [3, 6, 9, 12])
>>> a.append(99)
>>> a
array('i', [3, 6, 9, 12, 99])
>>> a.extend([6, 12, 18])
>>> a
array('i', [3, 9, 12, 99, 6, 6, 12, 18]) 
>>> a.remove(6)               #移除第一個 6
>>> a
array('i', [3, 9, 12, 99, 6, 12, 18])
>>> a.remove(6)               #移除第二個 6
>>> a
array('i', [3, 9, 12, 99, 12, 18])
>>> a.pop(2)                     #傳回索引 2 元素並刪除之
12
>>> a
array('i', [3, 9, 99, 12, 18])

但 array.array 物件與 List 都不適合直接進行向量運算, 例如向量乘以一個純量會得到另一個向量, 其元素是每一個原向量元素乘以該純量, 但直接用 List 或 array.array 物件乘以一個正整數會使元素倍增; 乘以一個實數則會出現錯誤, 例如 :

串列不是向量 :

>>> list1=[1,2,3] 
>>> list1 * 3
[1, 2, 3, 1, 2, 3, 1, 2, 3]       #若是向量應該得到 [3, 6, 9]

array 物件不是向量 :

>>> import array 
>>> arr1=array.array('i', [1,2,3])   #建立一個整數陣列
>>> arr1 * 3
array('i', [1, 2, 3, 1, 2, 3, 1, 2, 3])      #若是向量應該得到 [3, 6, 9]

向量運算預期 (1, 2, 3) * 3 得到 (3, 6, 9), 但上述範例顯示 List 與 array.array 物件都不是向量, 不能直接進行向量運算, 必須借助自訂函數處理才行. 而且 array.array 只能處理一維陣列, 無法處理矩陣或高維陣列, 傳入 2 維 List 給 array.array() 會出錯, 例如 :

>>> b=array.array('i', [[1, 2, 3], [4, 5, 6]])    #只能傳入一維串列
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
TypeError: an integer is required (got type list)

因此若要做向量, 矩陣或高維陣列運算, 必須用串列與自訂函數來模擬. 在 "Data Science from Scratch 中文版 : 用 Python 學資料科學" 這本書的第四章 "線性代數" 介紹了幾個自訂函數可將串列 List 當成向量來進行向量與矩陣運算 :


Source : 博客來


例如 :


測試 1 : 向量乘以純量

def vector_multiply_scalar(s, v):
    """ s=scalar, v=pseudo vector(list)"""
    return [s*vi for vi in v]

v1=[1,2,3]
v2=vector_multiply_scalar(3, v1)
print("{} {}".format(type(v2), v2))
v3=vector_multiply_scalar(3.1, v1)
print("{} {}".format(type(v3), v3))

此程式以自訂函數 vector_multiply_scalar(s, v) 模擬串列的向量運算, 第一參數 s 為純量, 可傳入整數或實數, 第二參數為偽向量, 傳入值是串列, 使用串列推導式 for 迴圈計算純量與每一個串列元素的乘積, 然後傳回結果串列, 執行結果如下 :

<class 'list'> [3, 6, 9]
<class 'list'> [3.1, 6.2, 9.3]

兩個相同維度的向量可以相加減, 也可以進行點積 (dot) 運算, 這會用到 Python 內建的好用函數 zip(), 此函數會將傳入的數個可迭代物件之元素一一配對成 tuple 後傳回可迭代之 Zip 物件, 可用 list() 函數轉成串列, 例如 :

>>> a=[1,2,3]
>>> b=[4,5,6]
>>> c=[7,8,9]
>>> d=zip(a, b, c)
>>> type(d)
<class 'zip'>
>>> list(d)
[(1, 4, 7), (2, 5, 8), (3, 6, 9)]

參考 :

https://docs.python.org/3.3/library/functions.html#zip

我將書上向量相加減與點積之自訂函數改寫與測試如下 :


測試 2 : 向量相加減與點積運算

def vector_multiply_scalar(s, v):
    """s=scalar, v=pseudo vector(list)"""
    return [s*vi for vi in v]

def vector_add(v1, v2):
    """v1, v2=pseudo vector(list)"""
    return [v1i + v2i for v1i, v2i in zip(v1, v2)]

def vector_substract(v1, v2):
    """v1, v2=pseudo vector(list)"""
    return [v1i - v2i for v1i, v2i in zip(v1, v2)]

def vector_dot(v1, v2):
    """v1, v2=pseudo vector(list)"""
    return sum(v1i * v2i for v1i, v2i in zip(v1, v2))

v1=[1,2,3]
v2=[4,5,6]
print("vector_add: {}".format(vector_add(v1, v2)))
print("vector_substract: {}".format(vector_substract(v1, v2)))
print("vector_dot: {}".format(vector_dot(v1, v2)))

執行結果 :

vector_add: [5, 7, 9]
vector_substract: [-3, -3, -3]
vector_dot: 32

以上是以串列模擬向量之操作, 使用二維串列當偽矩陣的做法更麻煩, 更不用說高維陣列了, 這就是發展 Numpy 套件的原因, 它可以更快速更有效率地解決向量, 矩陣, 與張量之運算.

參考 :

Python Arrays
numpy: list, array, matrix小结
Indexing vectors and arrays in Python
從零開始學資料科學:Numpy 基礎入門
Should I use scipy.pi, numpy.pi, or math.pi?
Quick Tip: The Difference Between a List and an Array in Python

沒有留言 :