過去有段時間,我對資料運算都不太有慧根,最大的原因在於,我總認為自己沒有什麼需要分析的資料,雖然介紹NumPy工具之類的文件或書籍,會使用一些開源或虛擬的資料集,接下來就是一連串的資料處理流程,然而,那些並不是我的需求。

而我始終的疑問就是,資料處理的需求本身,真的會直接面對資料嗎?

Matplotlib與海龜

不久前,我接觸到陣列程式設計的觀念(參考先前專欄〈陣列程式設計〉),於是,我重新以這個典範來認識NumPy,在資料運用上,算是有了些手感,因為我沒什麼想分析的資料。

另一方面,也基於個人的興趣,我突發奇想,試著用NumPy典範,以及Matplotlib,來實作繪圖相關的議題,而為了能夠在Matplotlib進行繪圖處理,因此,我必須透過NumPy,來產生繪圖時所需的相關資料。

事實上,有些繪圖上必要的資料,可以來自另一筆資料的轉換,像是Perlin噪聲,是從座標資訊轉換為噪聲值。

然而,有些需求就不是這樣了,像是實現海龜繪圖,舉例來說,Python內建的turtle模組,提供了海龜繪圖的實作,若匯入turtle全部的函式後,以下可以畫出星狀圖:

for _ in range(37):

     forward(200)

        left(170)

想試著用Matplotlib來繪出相同圖案的話,直覺上是用plot畫出線圖,這需要收集海龜每次移動後的座標資訊。

暫且不管NumPy的話,這個需求並不難,我們可以在上面的for迴圈中,透過turtle模組的pos函式取得座標,將座標收集在list,之後,從中將x座標與y座標分離出來,作為plot兩個軸的引數,就能在Matplotlib畫出相同的星狀圖。

NumPy與海龜

海龜繪圖一開始看似沒有資料,然而方才簡單的分析初步找到了資料的來源,也就是海龜的座標資訊,接著,我們要將座標轉換為x軸與y軸,可以用for迴圈來處理,或者透過NumPy。

例如,如果將座標收集在coord,此時,我們可以使用arr=np.array(coord),來建立NumPy陣列,而這麼一來,arr[0:,0]就會是x軸資料,arr[0:,1]就會是y軸資料。

但是,在我看來,這不夠NumPy,因為那個for _ in range(37)用到了迴圈,而我想消除迴圈的使用。之所以要避免迴圈的意義,其實,是在於想要儘量透過Universal函式來處理資料,也就是獨立地看待每一筆資料。

只不過,為了能透過Universal函式,表示在操作海龜前,還須有一筆資料作為來源,這該是什麼資料呢?

觀察上面的程式,你迭代了37次,而每次迭代會取得一次海龜座標,也就是說,實際上,是將數字0到36逐一轉換為海龜座標,因此,一開始的資料來源,就只是s=np.arange(37)。如果我們使用以下的程式片段:

def fd_lt(_, leng, a):

      forward(leng); left(a)

      return np.array(pos())

fd_lt = np.frompyfunc(fd_lt, 3, 1)

coord = np.array(fd_lt(s, 200, 170).

tolist())

就可以不透過迴圈取得coord了,如果fd_lt不是你實作的,只要將之視為黑箱,當它是個接受引數、傳回運算後結果的函式,就可以了;只不過fd_lt是我實作的,它使用了forward、left、pos,讓我感覺是有點作弊的方式。

畢竟,前兩者是有副作用的函式,改變了內部海龜的狀態,而且,實際上也沒用第一個引數,這些在我看來,都……很不NumPy!

轉換為向量資料

所謂很不NumPy的意思是,forward、left看來只是指令,並不像是在做資料的處理與轉換,如果我們想變得更NumPy一些,就要設法知道forward、left對於海龜的狀態做了什麼,也就是,必須先定義海龜的狀態。

海龜狀態基本上要有目前的座標與方向,在資料的表現上,我們直覺會想到,座標是二維座標,而方向用角度來表示,不過,採用向量來表現,將會變得更直覺,也就是,關於座標與方向,我們都用向量來表現。

這是因為進一步地,位移也就可以使用向量來表示,新座標就是座標向量與位移向量相加後的結果,想要得到位移向量的話,方向向量若是單位向量,只要乘上前進的長度就可以取到,而海龜在旋轉時,會改變方向向量,方向向量的旋轉可以藉由乘上旋轉矩陣來得到。

也就是說,forward、left指令,其實可以轉換為一連串的運算,而如果我們將一切組合起來之後,就可以將s=np.arange(37)以純計算的方式,轉換為x軸與y軸的資料:

   angle = s * 170

   dx = 200 * np.cos(angle)

   dy = 200 * np.sin(angle)

   x, y = np.cumsum(dx), np.cumsum(dy)

單就繪製星狀圖而言,分析到這邊就可以了,只不過談到海龜繪圖,進一步地會令人想到碎形,如果我們想以海龜來繪製碎形,往往又會涉及遞迴,然而,遞迴的運用在概念上,與使用了迴圈類似,但就NumPy風格而言,應該避免,這時,我們該怎麼做?

其實,指令本身可以看成符號,符號其實是一種資料,遞迴則是代表著符號的擴展,而這就讓我想到了L-system(可參考先前專欄〈碎形與L-system〉)。

例如,遞迴樹的L-system,可以從X開始,以規則X→F[+X][-X]擴展,若擴展為F[+F[+F][-F]][-F[+F][-F]],就可以進一步想辦法對應至資料處理,最後得到x軸與y軸的資料。

一切皆為資料

使用NumPy這類資料處理工具,並不在於API如何使用,而在於如何重新看待需求。

有時,你會直接面對資料,然而,我們必須重新看待資料處理的過程,而透過陣列程式設計的典範來約束,可以是這個過程中非常好的輔助。

不過,有時你面對的需求,似乎不是直接面對資料?其實,並非如此,確切地來說,你只是沒看出資料在哪,畢竟在電腦中,一切都是資料。

就陣列程式設計的典範而言,也是「一切皆為資料」,只不過找出資料前的分析過程,往往會比實作程式來得耗費時間。

這時,若我們只想使用NumPy的部份特性(像是廣播機制),或者方便的API來解決需求……基本上,這本身並不是難事,卻會失去進一步認識需求,或理解資料的可能性。

找出資料前的分析、將一切視為資料的過程,雖然耗費時間,卻有機會更深入地思考、挖掘需求或更多資料,也會是使用NumPy這類工具真正有趣的地方。

專欄作者

熱門新聞

Advertisement