想使用 Python 來處理大量資料,往往會建議使用 NumPy,只不過一直以來,我僅將 NumPy 當成是純綷的程式庫來看待,缺乏系統性的消化與整理,沒多久就又忘得一乾二淨…XD
後來我才知道,NumPy 不單只是其高效性、便利的 API,更重要的是它支持陣列程式設計(Array programming)典範,不能只是用程式設計者的角度來使用它,也要轉換一下看待資料的方式、處理資料時的角度。
或者試著轉換自己為數學家、科學家、物理學家等角色,如何才能用 NumPy 表現出自己的想法,而不是用 NumPy 表現出程式設計者的想法。
例如,現在有一組數字 [1, 2, 3]
,你要怎麼全部加 10 呢?命令式(Imperative)典範的寫法是:
nums = [1, 2, 3]
for i in range(len(nums)):
nums[i] += 10
如果想要一些函數式(Functional)的概念,或許你會使用 for comprehension:
nums1 = [1, 2, 3]
nums2 = [n + 10 for n in nums1]
不管是命令式或函數式,很大程度上,都是從程式設計者的角度,從程式流程設計的角度來表現想法,畢竟 for comprehension 本質上也是一種重複執行的概念,或許你會說,封裝起來呢?例如 Python 本身就有個 map
函式:
nums1 = [1, 2, 3]
nums2 = list(map(lambda n: n + 10, nums1))
雖然沒有 for
了,不過你還是得使用 lambda
,這是程式設計上一級函式的概念,更何況你還得知道 map
傳回的是個產生器,得用 list
迭代出每個元素,這些對表達想法有幫助嗎?
本質上,以上的寫法,無論是命令式或是函數式,都是在處理資料時,獨立地針對每個純量(變數值)進行處理,而不是將 nums
當成一個整體來看待。
如果使用 NumPy 呢?
import numpy as np
nums1 = np.array([1, 2, 3])
nums2 = nums1 + 10
從程式設計者的角度來看,nums
是一組數字,怎麼可以直接加上一個純量呢?然而,換個角度來想,
你是要對這組數字進行相同的操作,直接加 10,在表達上不是就夠了?
如果想將兩組或多組的資料,依元素位置進行加減乘除,這麼寫比較方便呢?
nums1 = [1, 2, 3]
nums2 = [4, 5, 6]
nums3 = [nums1[i] + nums2[i] for i in range(len(nums1))]
還是這麼寫比較方便呢?
import numpy as np
nums1 = np.array([1, 2, 3])
nums2 = np.array([4, 5, 6])
nums3 = nums1 + nums2
若想利用 NumPy,你得思考一下資料的架構方式,從個別的資料中整理出一組一組的資料,每一組資料能以相同方式來操作。
如何整理資料,基本上是在動手寫程式之前就要做的,真正整理資料時,可以寫程式來處理,接著才是套用 NumPy 來處理資料,一組一組的資料,在 NumPy 或者一些支援這類典範的語言或工具中,通常是用名為陣列的結構來處理,因此這類典範,也稱為陣列程式設計。
你說這跟函數式設計中,filter
、map
、reduce
的概念有什麼不同呢?如果你單純將 [1, 2, 3]
視為一組數字,那就只是 filter
、map
、reduce
層次的概念,如果 [1, 2, 3]
實際上代表向量呢?例如,它就是表示三維空間中的一個方向向量?
在陣列程式設計中,一組數字不見得就是一組數字,進一步地,還可以用向量的角度來思考,這麼一來,你就可以對它進行向量縮放、相加、點積、叉積等運算:
import numpy as np
v1 = np.array([1, 2, 3]) # 以列(row)表示的向量
v2 = np.array([4, 5, 6])
v3 = v1 * 2 # 縮放
v4 = v1 + v2 # 相加
v5 = np.dot(v1, v2) # 點積
v6 = np.cross(v1, v2) # 叉積
進一步地,一組數字也可以是個矩陣,可以進行矩陣相乘,注意,矩陣相乘處理使用的是 @
運算子(而不是 *
):
import numpy as np
m1 = np.array([
[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9, 10, 11, 12],
[13, 14, 15, 16]
])
m2 = np.array([
[13, 14, 15, 16],
[ 9, 10, 11, 12],
[ 5, 6, 7, 8],
[ 1, 2, 3, 4]
])
m3 = m1 @ m2 # 也可以寫 np.dot(m1, m2)
來個位移矩陣與向量的運算吧!若 tx
、ty
分別為 x
、y
部份的位移量,位移矩陣與向量的運算(可參考〈認識矩陣〉)就可以是:
import numpy as np
tx = 10
ty = 5
x = 10
y = 20
t = np.array([
[1, 0, tx],
[0, 1, ty],
[0, 0, 1]
])
v = t @ np.array([x, y, 1])
簡單來說,改變自己的想法,從陣列程式設計來思考資料的處理,接著再看看 NumPy,就會比較清楚該怎麼運用 NumPy。