天天看點

NumPy的詳細教程 2 NumPy-快速處理資料

如果你想要運作教程中的示例,你至少需要在你的電腦上安裝了以下一些軟體:

<a target="_blank" href="http://www.python.org/">python</a>

<a target="_blank" href="http://numpy.scipy.org/">numpy</a>

這些是可能對你有幫助的:

numpy的主要對象是同種元素的多元數組。這是一個所有的元素都是一種類型、通過一個正整數元組索引的元素表格(通常是元素是數字)。在numpy中次元(dimensions)叫做軸(axes),軸的個數叫做秩(rank)。

例如,在3d空間一個點的坐标<code>[1, 2, 3]</code>是一個秩為1的數組,因為它隻有一個軸。那個軸長度為3.又例如,在以下例子中,數組的秩為2(它有兩個次元).第一個次元長度為2,第二個次元長度為3.

numpy的數組類被稱作ndarray。通常被稱作數組。注意numpy.array和标準python庫類array.array并不相同,後者隻處理一維數組和提供少量功能。更多重要ndarray對象屬性有:

ndarray.ndim

數組軸的個數,在python的世界中,軸的個數被稱作秩

ndarray.shape

數組的次元。這是一個訓示數組在每個次元上大小的整數元組。例如一個n排m列的矩陣,它的shape屬性将是(2,3),這個元組的長度顯然是秩,即次元或者ndim屬性

ndarray.size

數組元素的總個數,等于shape屬性中元組元素的乘積。

ndarray.dtype

一個用來描述數組中元素類型的對象,可以通過創造或指定dtype使用标準python類型。另外numpy提供它自己的資料類型。

ndarray.itemsize

數組中每個元素的位元組大小。例如,一個元素類型為float64的數組itemsiz屬性值為8(=64/8),又如,一個元素類型為complex32的數組item屬性為4(=32/8).

ndarray.data

包含實際數組元素的緩沖區,通常我們不需要使用這個屬性,因為我們總是通過索引來使用數組中的元素。

有好幾種建立數組的方法。

例如,你可以使用<code>array</code>函數從正常的python清單和元組創造數組。所建立的數組類型由原序列中的元素類型推導而來。

數組将序列包含序列轉化成二維的數組,序列包含序列包含序列轉化成三維數組等等。

數組類型可以在建立時顯示指定

通常,數組的元素開始都是未知的,但是它的大小已知。是以,numpy提供了一些使用占位符建立數組的函數。這最小化了擴充數組的需要和高昂的運算代價。

函數<code>function</code>建立一個全是0的數組,函數<code>ones</code>建立一個全1的數組,函數<code>empty</code>建立一個内容随機并且依賴與記憶體狀态的數組。預設建立的數組類型(dtype)都是float64。

為了建立一個數列,numpy提供一個類似arange的函數傳回數組而不是清單:

當<code>arange</code>使用浮點數參數時,由于有限的浮點數精度,通常無法預測獲得的元素個數。是以,最好使用函數<code>linspace</code>去接收我們想要的元素個數來代替用range來指定步長。

當你列印一個數組,numpy以類似嵌套清單的形式顯示它,但是呈以下布局:

最後的軸從左到右列印

次後的軸從頂向下列印

剩下的軸從頂向下列印,每個切片通過一個空行與下一個隔開

一維數組被列印成行,二維數組成矩陣,三維數組成矩陣清單。

檢視形狀操作一節獲得有關reshape的更多細節

如果一個數組用來列印太大了,numpy自動省略中間部分而隻列印角落

禁用numpy的這種行為并強制列印整個數組,你可以設定printoptions參數來更改列印選項。

數組的算術運算是按元素的。新的數組被建立并且被結果填充。

不像許多矩陣語言,numpy中的乘法運算符<code>*</code>訓示按元素計算,矩陣乘法可以使用<code>dot</code>函數或建立矩陣對象實作(參見教程中的矩陣章節)

有些操作符像<code>+=</code>和<code>*=</code>被用來更改已存在數組而不建立一個新的數組。

當運算的是不同類型的數組時,結果數組和更普遍和精确的已知(這種行為叫做upcast)。

這些運算預設應用到數組好像它就是一個數字組成的清單,無關數組的形狀。然而,指定<code>axis</code>參數你可以吧運算應用到數組指定的軸上:

numpy提供常見的數學函數如<code>sin</code>,<code>cos</code>和<code>exp</code>。在numpy中,這些叫作“通用函數”(ufunc)。在numpy裡這些函數作用按數組的元素運算,産生一個數組作為輸出。

更多函數all, alltrue, any, apply along axis, argmax, argmin, argsort, average, bincount, ceil, clip, conj, conjugate, corrcoef, cov, cross, cumprod, cumsum, diff, dot, floor, inner, inv, lexsort, max, maximum, mean, median, min, minimum, nonzero, outer, prod, re,

多元數組可以每個軸有一個索引。這些索引由一個逗号分割的元組給出。

當少于軸數的索引被提供時,确失的索引被認為是整個切片:

<code>b[i]</code>中括号中的表達式被當作<code>i</code>和一系列<code>:</code>,來代表剩下的軸。numpy也允許你使用“點”像<code>b[i,...]</code>。

點(…)代表許多産生一個完整的索引元組必要的分号。如果x是秩為5的數組(即它有5個軸),那麼:

x[1,2,…] 等同于 x[1,2,:,:,:],

x[…,3] 等同于 x[:,:,:,:,3]

x[4,…,5,:] 等同 x[4,:,:,5,:].

然而,如果一個人想對每個數組中元素進行運算,我們可以使用flat屬性,該屬性是數組元素的一個疊代器:

一個數組的形狀由它每個軸上的元素個數給出:

一個數組的形狀可以被多種指令修改:

<code>reshape</code>函數改變參數形狀并傳回它,而<code>resize</code>函數改變數組自身。

如果在改變形狀操作中一個次元被給做-1,其次元将自動被計算

幾種方法可以沿不同軸将數組堆疊在一起:

函數<code>column_stack</code>以列将一維數組合成二維數組,它等同與<code>vstack</code>對一維數組。

<code>row_stack</code>函數,另一方面,将一維數組以行組合成二維數組。

對那些次元比二維更高的數組,<code>hstack</code>沿着第二個軸組合,<code>vstack</code>沿着第一個軸組合,<code>concatenate</code>允許可選參數給出組合時沿着的軸。

note

在複雜情況下,<code>r_[]</code>和<code>c_[]</code>對建立沿着一個方向組合的數很有用,它們允許範圍符号(“:”):

當使用數組作為參數時,<code>r_</code>和<code>c_</code>的預設行為和<code>vstack</code>和<code>hstack</code>很像,但是允許可選的參數給出組合所沿着的軸的代号。

使用<code>hsplit</code>你能将數組沿着它的水準軸分割,或者指定傳回相同形狀數組的個數,或者指定在哪些列後發生分割:

<code>vsplit</code>沿着縱向的軸分割,<code>array split</code>允許指定沿哪個軸分割。

當運算和處理數組時,它們的資料有時被拷貝到新的數組有時不是。這通常是新手的困惑之源。這有三種情況:

簡單的指派不拷貝數組對象或它們的資料。

不同的數組對象分享同一個資料。視圖方法創造一個新的數組對象指向同一資料。

切片數組傳回它的一個視圖:

這個複制方法完全複制數組和它的資料。

建立數組

轉化

操作

詢問

排序

運算

基本統計

基本線性代數

廣播法則能使通用函數有意義地處理不具有相同形狀的輸入。

廣播第一法則是,如果所有的輸入數組次元不都相同,一個“1”将被重複地添加在次元較小的數組上直至所有的數組擁有一樣的次元。

廣播第二法則确定長度為1的數組沿着特殊的方向表現地好像它有沿着那個方向最大形狀的大小。對數組來說,沿着那個次元的數組元素的值理應相同。

numpy比普通python序列提供更多的索引功能。除了索引整數和切片,正如我們之前看到的,數組可以被整數數組和布爾數組索引。

我們也可以給出不不止一維的索引,每一維的索引數組必須有相同的形狀。

自然,我們可以把i和j放到序列中(比如說清單)然後通過list索引。

然而,我們不能把i和j放在一個數組中,因為這個數組将被解釋成索引a的第一維。

你也可以使用數組索引作為目标來指派:

然而,當一個索引清單包含重複時,指派被多次完成,保留最後的值:

這足夠合理,但是小心如果你想用python的<code>+=</code>結構,可能結果并非你所期望:

即使0在索引清單中出現兩次,索引為0的元素僅僅增加一次。這是因為python要求<code>a+=1</code>和<code>a=a+1</code>等同。

當我們使用整數數組索引數組時,我們提供一個索引清單去選擇。通過布爾數組索引的方法是不同的我們顯式地選擇數組中我們想要和不想要的元素。

我們能想到的使用布爾數組的索引最自然方式就是使用和原數組一樣形狀的布爾數組。

這個屬性在指派時非常有用:

第二種通過布爾來索引的方法更近似于整數索引;對數組的每個次元我們給一個一維布爾數組來選擇我們想要的切片。

你也可以實行如下簡化:

然後這樣使用它:

繼續前進,基本線性代數包含在這裡。

參考numpy檔案夾中的linalg.py獲得更多資訊

這是一個關于矩陣類的簡短介紹。

注意numpy中數組和矩陣有些重要的差別。numpy提供了兩個基本的對象:一個n維數組對象和一個通用函數對象。其它對象都是建構在它們之上 的。特别的,矩陣是繼承自numpy數組對象的二維數組對象。對數組和矩陣,索引都必須包含合适的一個或多個這些組合:整數标量、省略号 (ellipses)、整數清單;布爾值,整數或布爾值構成的元組,和一個一維整數或布爾值數組。矩陣可以被用作矩陣的索引,但是通常需要數組、清單或者 其它形式來完成這個任務。

讓我們建立數組和矩陣用來切片:

現在,讓我們簡單的切幾片。基本的切片使用切片對象或整數。例如,<code>a[:]</code>和<code>m[:]</code>的求值将表現得和python索引很相似。然而要注意很重要的一點就是numpy切片數組不建立資料的副本;切片提供統一資料的視圖。

現在有些和python索引不同的了:你可以同時使用逗号分割索引來沿着多個軸索引。

假如我們想要一個數組的第一列和第三列,一種方法是使用清單切片:

稍微複雜點的方法是使用<code>take()</code>方法(method):

如果我們想跳過第一行,我們可以這樣:

或者我們僅僅使用<code>a[1:,[1,3]]</code>。還有一種方法是通過矩陣向量積(叉積)。

為了讀者的友善,在次寫下之前的矩陣:

現在讓我們做些更複雜的。比如說我們想要保留第一行大于1的列。一種方法是建立布爾索引:

就是我們想要的!但是索引矩陣沒這麼友善。

如果我們想要在矩陣兩個方向有條件地切片,我們必須稍微調整政策,代之以:

我們需要使用向量積<code>ix_</code>:

下面我們給出簡短和有用的提示。

更改數組的次元,你可以省略一個尺寸,它将被自動推導出來。

我們如何用兩個相同尺寸的行向量清單建構一個二維數組?在matlab中這非常簡單:如果x和y是兩個相同長度的向量,你僅僅需要做<code>m=[x;y]</code>。在numpy中這個過程通過函數<code>column_stack</code>、<code>dstack</code>、<code>hstack</code>和<code>vstack</code>來完成,取決于你想要在那個次元上組合。例如:

二維以上這些函數背後的邏輯會很奇怪。

numpy中<code>histogram</code>函數應用到一個數組傳回一對變量:直方圖數組和箱式向量。注意:<code>matplotlib</code>也有一個用來建立直方圖的函數(叫作<code>hist</code>,正如matlab中一樣)與numpy中的不同。主要的差别是<code>pylab.hist</code>自動繪制直方圖,而<code>numpy.histogram</code>僅僅産生資料。

NumPy的詳細教程 2 NumPy-快速處理資料

标準安裝的python中用清單(list)儲存一組值,可以用來當作數組使用,不過由于清單的元素可以是任何對象,是以清單中所儲存的是對象的指針。這樣為了儲存一個簡單的[1,2,3],需要有3個指針和三個整數對象。對于數值運算來說這種結構顯然比較浪費記憶體和cpu計算時間。

此外python還提供了一個array子產品,array對象和清單不同,它直接儲存數值,和c語言的一維數組比較類似。但是由于它不支援多元,也沒有各種運算函數,是以也不适合做數值運算。

numpy的誕生彌補了這些不足,numpy提供了兩種基本的對象:ndarray(n-dimensional array object)和 ufunc(universal function object)。ndarray(下文統一稱之為數組)是存儲單一資料類型的多元數組,而ufunc則是能夠對數組進行處理的函數。

NumPy的詳細教程 2 NumPy-快速處理資料

函數庫的導入

本書的示例程式假設用以下推薦的方式導入numpy函數庫:

NumPy的詳細教程 2 NumPy-快速處理資料

首先需要建立數組才能對其進行其它操作。

我們可以通過給array函數傳遞python的序列對象建立數組,如果傳遞的是多層嵌套的序列,将建立多元數組(下例中的變量c):

數組的大小可以通過其shape屬性獲得:

數組a的shape隻有一個元素,是以它是一維數組。而數組c的shape有兩個元素,是以它是二維數組,其中第0軸的長度為3,第1軸的長度為4。還可以通過修改數組的shape屬性,在保持數組元素個數不變的情況下,改變數組每個軸的長度。下面的例子将數組c的shape改為(4,3),注意從(3,4)改為(4,3)并不是對數組進行轉置,而隻是改變每個軸的大小,數組元素在記憶體中的位置并沒有改變:

當某個軸的元素為-1時,将根據數組元素的個數自動計算此軸的長度,是以下面的程式将數組c的shape改為了(2,6):

使用數組的reshape方法,可以建立一個改變了尺寸的新數組,原數組的shape保持不變:

數組a和d其實共享資料存儲記憶體區域,是以修改其中任意一個數組的元素都會同時修改另外一個數組的内容:

數組的元素類型可以通過dtype屬性獲得。上面例子中的參數序列的元素都是整數,是以所建立的數組的元素類型也是整數,并且是32bit的長整型。可以通過dtype參數在建立時指定元素類型:

上面的例子都是先建立一個python序列,然後通過array函數将其轉換為數組,這樣做顯然效率不高。是以numpy提供了很多專門用來建立數組的函數。下面的每個函數都有一些關鍵字參數,具體用法請檢視函數說明。

arange函數類似于python的range函數,通過指定開始值、終值和步長來建立一維數組,注意數組不包括終值:

linspace函數通過指定開始值、終值和元素個數來建立一維數組,可以通過endpoint關鍵字指定是否包括終值,預設設定是包括終值:

logspace函數和linspace類似,不過它建立等比數列,下面的例子産生1(10^0)到100(10^2)、有20個元素的等比數列:

此外,使用frombuffer, fromstring, fromfile等函數可以從位元組序列建立數組,下面以fromstring為例:

python的字元串實際上是位元組序列,每個字元占一個位元組,是以如果從字元串s建立一個8bit的整數數組的話,所得到的數組正好就是字元串中每個字元的ascii編碼:

如果從字元串s建立16bit的整數數組,那麼兩個相鄰的位元組就表示一個整數,把位元組98和位元組97當作一個16位的整數,它的值就是98*256+97 = 25185。可以看出記憶體中是以little endian(低位位元組在前)方式儲存資料的。

如果把整個字元串轉換為一個64位的雙精度浮點數數組,那麼它的值是:

顯然這個例子沒有什麼意義,但是可以想象如果我們用c語言的二進制方式寫了一組double類型的數值到某個檔案中,那們可以從此檔案讀取相應的資料,并通過fromstring函數将其轉換為float64類型的數組。

我們可以寫一個python的函數,它将數組下标轉換為數組中對應的值,然後使用此函數建立數組:

fromfunction函數的第一個參數為計算每個數組元素的函數,第二個參數為數組的大小(shape),因為它支援多元數組,是以第二個參數必須是一個序列,本例中用(10,)建立一個10元素的一維數組。

下面的例子建立一個二維數組表示九九乘法表,輸出的數組a中的每個元素a[i, j]都等于func2(i, j):

NumPy的詳細教程 2 NumPy-快速處理資料

數組元素的存取方法和python的标準方法相同:

和python的清單序列不同,通過下标範圍擷取的新的數組是原始數組的一個視圖。它與原始數組共享同一塊資料空間:

除了使用下标範圍存取元素之外,numpy還提供了兩種存取元素的進階方法。

使用整數序列

當使用整數序列對數組元素進行存取時,将使用整數序列中的每個元素作為下标,整數序列可以是清單或者數組。使用整數序列作為下标獲得的數組不和原始數組共享資料空間。

使用布爾數組

當使用布爾數組b作為下标存取數組x中的元素時,将收集數組x中所有在數組b中對應下标為true的元素。使用布爾數組作為下标獲得的數組不和原始數組共享資料空間,注意這種方式隻對應于布爾數組,不能使用布爾清單。

布爾數組一般不是手工産生,而是使用布爾運算的ufunc函數産生,關于ufunc函數請參照 ufunc運算 一節。

NumPy的詳細教程 2 NumPy-快速處理資料

組元不需要圓括号

雖然我們經常在python中用圓括号将組元括起來,但是其實組元的文法定義隻需要用逗号隔開即可,例如 x,y=y,x 就是用組元交換變量值的一個例子。

NumPy的詳細教程 2 NumPy-快速處理資料

圖2.1 使用數組切片文法通路多元數組中的元素

如何建立這個數組

你也許會對如何建立a這樣的數組感到好奇,數組a實際上是一個加法表,縱軸的值為0, 10, 20, 30, 40, 50;橫軸的值為0, 1, 2, 3, 4, 5。縱軸的每個元素都和橫軸的每個元素求和,就得到圖中所示的數組a。你可以用下面的語句建立它,至于其原理我們将在後面的章節進行讨論:

多元數組同樣也可以使用整數序列和布爾數組進行存取。

NumPy的詳細教程 2 NumPy-快速處理資料

圖2.2 使用整數序列和布爾數組通路多元數組中的元素

a[(0,1,2,3,4),(1,2,3,4,5)] : 用于存取數組的下标和仍然是一個有兩個元素的組元,組元中的每個元素都是整數序列,分别對應數組的第0軸和第1軸。從兩個序列的對應位置取出兩個整數組成下标: a[0,1], a[1,2], ..., a[4,5]。

a[3:, [0, 2, 5]] : 下标中的第0軸是一個範圍,它選取第3行之後的所有行;第1軸是整數序列,它選取第0, 2, 5三列。

a[mask, 2] : 下标的第0軸是一個布爾數組,它選取第0,2,5行;第1軸是一個整數,選取第2列。

NumPy的詳細教程 2 NumPy-快速處理資料

在c語言中我們可以通過struct關鍵字定義結構類型,結構中的字段占據連續的記憶體空間,每個結構體占用的記憶體大小都相同,是以可以很容易地定義結構數組。和c語言一樣,在numpy中也很容易對這種結構數組進行操作。隻要numpy中的結構定義和c語言中的定義相同,numpy就可以很友善地讀取c語言的結構數組的二進制資料,轉換為numpy的結構數組。

假設我們需要定義一個結構數組,它的每個元素都有name, age和weight字段。在numpy中可以如下定義:

我們先建立一個dtype對象persontype,通過其字典參數描述結構類型的各個字段。字典有兩個關鍵字:names,formats。每個關鍵字對應的值都是一個清單。names定義結構中的每個字段名,而formats則定義每個字段的類型:

s32 : 32個位元組的字元串類型,由于結構中的每個元素的大小必須固定,是以需要指定字元串的長度

i : 32bit的整數類型,相當于np.int32

f : 32bit的單精度浮點數類型,相當于np.float32

然後我們調用array函數建立數組,通過關鍵字參數 dtype=persontype, 指定所建立的數組的元素類型為結構persontype。運作上面程式之後,我們可以在ipython中執行如下的語句檢視數組a的元素類型

這裡我們看到了另外一種描述結構類型的方法: 一個包含多個組元的清單,其中形如 (字段名, 類型描述) 的組元描述了結構中的每個字段。類型描述前面為我們添加了 '|', '&lt;' 等字元,這些字元用來描述字段值的位元組順序:

| : 忽視位元組順序

&lt; : 低位位元組在前

&gt; : 高位位元組在前

結構數組的存取方式和一般數組相同,通過下标能夠取得其中的元素,注意元素的值看上去像是組元,實際上它是一個結構:

a[0]是一個結構元素,它和數組a共享記憶體資料,是以可以通過修改它的字段,改變原始數組中的對應字段:

結構像字典一樣可以通過字元串下标擷取其對應的字段值:

我們不但可以獲得結構元素的某個字段,還可以直接獲得結構數組的字段,它傳回的是原始數組的視圖,是以可以通過修改b[0]改變a[0]["age"]:

通過調用a.tostring或者a.tofile方法,可以直接輸出數組a的二進制形式:

利用下面的c語言程式可以将test.bin檔案中的資料讀取出來。

記憶體對齊

c語言的結構體為了記憶體尋址友善,會自動的添加一些填充用的位元組,這叫做記憶體對齊。例如如果把下面的name[32]改為name[30]的話,由于記憶體對齊問題,在name和age中間會填補兩個位元組,最終的結構體大小不會改變。是以如果numpy中的所配置的記憶體大小不符合c語言的對齊規範的話,将會出現資料錯位。為了解決這個問題,在建立dtype對象時,可以傳遞參數align=true,這樣numpy的結構數組的記憶體對齊和c語言的結構體就一緻了。

結構類型中可以包括其它的結構類型,下面的語句建立一個有一個字段f1的結構,f1的值是另外一個結構,它有字段f2,其類型為16bit整數。

當某個字段類型為數組時,用組元的第三個參數表示,下面描述的f1字段是一個shape為(2,3)的雙精度浮點數組:

用下面的字典參數也可以定義結構類型,字典的關鍵字為結構中字段名,值為字段的類型描述,但是由于字典的關鍵字是沒有順序的,是以字段的順序需要在類型描述中給出,類型描述是一個組元,它的第二個值給出字段的位元組為機關的偏移量,例如age字段的偏移量為25個位元組:

NumPy的詳細教程 2 NumPy-快速處理資料
NumPy的詳細教程 2 NumPy-快速處理資料

圖2.3 ndarray數組對象在記憶體中的儲存方式

資料存儲區域儲存着數組中所有元素的二進制資料,dtype對象則知道如何将元素的二進制資料轉換為可用的值。數組的維數、大小等資訊都儲存在ndarray數組對象的資料結構中。圖中顯示的是如下數組的記憶體結構:

strides中儲存的是當每個軸的下标增加1時,資料存儲區中的指針所增加的位元組數。例如圖中的strides為12,4,即第0軸的下标增加1時,資料的位址增加12個位元組:即a[1,0]的位址比a[0,0]的位址要高12個位元組,正好是3個單精度浮點數的總位元組數;第1軸下标增加1時,資料的位址增加4個位元組,正好是單精度浮點數的位元組數。

如果strides中的數值正好和對應軸所占據的位元組數相同的話,那麼資料在記憶體中是連續存儲的。然而資料并不一定都是連續儲存的,前面介紹過通過下标範圍得到新的數組是原始數組的視圖,即它和原始視圖共享資料存儲區域:

由于數組b和數組a共享資料存儲區,而b中的第0軸和第1軸都是數組a中隔一個元素取一個,是以數組b的strides變成了24,8,正好都是數組a的兩倍。 對照前面的圖很容易看出資料0和2的位址相差8個位元組,而0和6的位址相差24個位元組。

元素在資料存儲區中的排列格式有兩種:c語言格式和fortan語言格式。在c語言中,多元數組的第0軸是最上位的,即第0軸的下标增加1時,元素的位址增加的位元組數最多;而fortan語言的多元數組的第0軸是最下位的,即第0軸的下标增加1時,位址隻增加一個元素的位元組數。在numpy中,元素在記憶體中的排列預設是以c語言格式存儲的,如果你希望改為fortan格式的話,隻需要給數組傳遞order="f"參數:

NumPy的詳細教程 2 NumPy-快速處理資料

ufunc是universal function的縮寫,它是一種能對數組的每個元素進行操作的函數。numpy内置的許多ufunc函數都是在c語言級别實作的,是以它們的計算速度非常快。讓我們來看一個例子:

先用linspace産生一個從0到2*pi的等距離的10個數,然後将其傳遞給sin函數,由于np.sin是一個ufunc函數,是以它對x中的每個元素求正弦值,然後将結果傳回,并且指派給y。計算之後x中的值并沒有改變,而是新建立了一個數組儲存結果。如果我們希望将sin函數所計算的結果直接覆寫到數組x上去的話,可以将要被覆寫的數組作為第二個參數傳遞給ufunc函數。例如::

sin函數的第二個參數也是x,那麼它所做的事情就是對x中的每給值求正弦值,并且把結果儲存到x中的對應的位置中。此時函數的傳回值仍然是整個計算的結果,隻不過它就是x,是以兩個變量的id是相同的(變量t和變量x指向同一塊記憶體區域)。

我用下面這個小程式,比較了一下numpy.math和python标準庫的math.sin的計算速度::

在我的電腦上計算100萬次正弦值,numpy.sin比math.sin快10倍多。這得利于numpy.sin在c語言級别的循環計算。numpy.sin同樣也支援對單個數值求正弦,例如:numpy.sin(0.5)。不過值得注意的是,對單個數的計算math.sin則比numpy.sin快得多了,讓我們看下面這個測試程式:

請注意numpy.sin的計算速度隻有math.sin的1/5。這是因為numpy.sin為了同時支援數組和單個值的計算,其c語言的内部實作要比math.sin複雜很多,如果我們同樣在python級别進行循環的話,就會看出其中的差别了。此外,numpy.sin傳回的數的類型和math.sin傳回的類型有所不同,math.sin傳回的是python的标準float類型,而numpy.sin則傳回一個numpy.float64類型:

通過上面的例子我們了解了如何最有效率地使用math庫和numpy庫中的數學函數。因為它們各有長短,是以在導入時不建議使用*号全部載入,而是應該使用import numpy as np的方式載入,這樣我們可以根據需要選擇合适的函數調用。

numpy中有衆多的ufunc函數為我們提供各式各樣的計算。除了sin這種單輸入函數之外,還有許多多個輸入的函數,add函數就是一個最常用的例子。先來看一個例子:

add函數傳回一個新的數組,此數組的每個元素都為兩個參數數組的對應元素之和。它接受第3個參數指定計算結果所要寫入的數組,如果指定的話,add函數就不再産生新的數組。

由于python的操作符重載功能,計算兩個數組相加可以簡單地寫為a+b,而np.add(a,b,a)則可以用a+=b來表示。下面是數組的運算符和其對應的ufunc函數的一個清單,注意除号"/"的意義根據是否激活__future__.division有所不同。

y = x1 + x2:

add(x1, x2 [, y])

y = x1 - x2:

subtract(x1, x2 [, y])

y = x1 * x2:

multiply (x1, x2 [, y])

y = x1 / x2:

divide (x1, x2 [, y]), 如果兩個數組的元素為整數,那麼用整數除法

true divide (x1, x2 [, y]), 總是傳回精确的商

y = x1 // x2:

floor divide (x1, x2 [, y]), 總是對傳回值取整

y = -x:

negative(x [,y])

y = x1**x2:

power(x1, x2 [, y])

y = x1 % x2:

remainder(x1, x2 [, y]), mod(x1, x2, [, y])

數組對象支援這些操作符,極大地簡化了算式的編寫,不過要注意如果你的算式很複雜,并且要運算的數組很大的話,會因為産生大量的中間結果而降低程式的運算效率。例如:假設a b c三個數組采用算式x=a*b+c計算,那麼它相當于:

也就是說需要産生一個數組t儲存乘法的計算結果,然後再産生最後的結果數組x。我們可以通過手工将一個算式分解為x = a*b; x += c,以減少一次記憶體配置設定。

通過組合标準的ufunc函數的調用,可以實作各種算式的數組計算。不過有些時候這種算式不易編寫,而針對每個元素的計算函數卻很容易用python實作,這時可以用frompyfunc函數将一個計算單個元素的函數轉換成ufunc函數。這樣就可以友善地用所産生的ufunc函數對數組進行計算了。讓我們來看一個例子。

NumPy的詳細教程 2 NumPy-快速處理資料

圖2.4 三角波可以用分段函數進行計算

我們很容易根據上圖所示寫出如下的計算三角波某點y坐标的函數:

顯然triangle_wave函數隻能計算單個數值,不能對數組直接進行處理。我們可以用下面的方法先使用清單包容(list comprehension),計算出一個list,然後用array函數将清單轉換為數組:

這種做法每次都需要使用清單包容文法調用函數,對于多元數組是很麻煩的。讓我們來看看如何用frompyfunc函數來解決這個問題:

frompyfunc的調用格式為frompyfunc(func, nin, nout),其中func是計算單個元素的函數,nin是此函數的輸入參數的個數,nout是此函數的傳回值的個數。雖然triangle_wave函數有4個參數,但是由于後三個c, c0, hc在整個計算中值都是固定的,是以所産生的ufunc函數其實隻有一個參數。為了滿足這個條件,我們用一個lambda函數對triangle_wave的參數進行一次包裝。這樣傳入frompyfunc的函數就隻有一個參數了。這樣子做,效率并不是太高,另外還有一種方法:

我們通過函數triangle_func包裝三角波的三個參數,在其内部定義一個計算三角波的函數trifunc,trifunc函數在調用時會采用triangle_func的參數進行計算。最後triangle_func傳回用frompyfunc轉換結果。

值得注意的是用frompyfunc得到的函數計算出的數組元素的類型為object,因為frompyfunc函數無法保證python函數傳回的資料類型都完全一緻。是以還需要再次 y2.astype(np.float64)将其轉換為雙精度浮點數組。

NumPy的詳細教程 2 NumPy-快速處理資料

當我們使用ufunc函數對兩個數組進行計算時,ufunc函數會對這兩個數組的對應元素進行計算,是以它要求這兩個數組有相同的大小(shape相同)。如果兩個數組的shape不同的話,會進行如下的廣播(broadcasting)處理:

讓所有輸入數組都向其中shape最長的數組看齊,shape中不足的部分都通過在前面加1補齊

輸出數組的shape是輸入數組shape的各個軸上的最大值

如果輸入數組的某個軸和輸出數組的對應軸的長度相同或者其長度為1時,這個數組能夠用來計算,否則出錯

當輸入數組的某個軸的長度為1時,沿着此軸運算時都用此軸上的第一組值

上述4條規則了解起來可能比較費勁,讓我們來看一個實際的例子。

先建立一個二維數組a,其shape為(6,1):

再建立一維數組b,其shape為(5,):

計算a和b的和,得到一個加法表,它相當于計算a,b中所有元素組的和,得到一個shape為(6,5)的數組:

由于a和b的shape長度(也就是ndim屬性)不同,根據規則1,需要讓b的shape向a對齊,于是将b的shape前面加1,補齊為(1,5)。相當于做了如下計算:

這樣加法運算的兩個輸入數組的shape分别為(6,1)和(1,5),根據規則2,輸出數組的各個軸的長度為輸入數組各個軸上的長度的最大值,可知輸出數組的shape為(6,5)。

由于b的第0軸上的長度為1,而a的第0軸上的長度為6,是以為了讓它們在第0軸上能夠相加,需要将b在第0軸上的長度擴充為6,這相當于:

由于a的第1軸的長度為1,而b的第一軸長度為5,是以為了讓它們在第1軸上能夠相加,需要将a在第1軸上的長度擴充為5,這相當于:

經過上述處理之後,a和b就可以按對應元素進行相加運算了。

當然,numpy在執行a+b運算時,其内部并不會真正将長度為1的軸用repeat函數進行擴充,如果這樣做的話就太浪費空間了。

由于這種廣播計算很常用,是以numpy提供了一個快速産生如上面a,b數組的方法: ogrid對象:

ogrid是一個很有趣的對象,它像一個多元數組一樣,用切片組元作為下标進行存取,傳回的是一組可以用來廣播計算的數組。其切片下标有兩種形式:

開始值:結束值:步長,和np.arange(開始值, 結束值, 步長)類似

開始值:結束值:長度j,當第三個參數為虛數時,它表示傳回的數組的長度,和np.linspace(開始值, 結束值, 長度)類似:

ogrid為什麼不是函數

根據python的文法,隻有在中括号中才能使用用冒号隔開的切片文法,如果ogrid是函數的話,那麼這些切片必須使用slice函數建立,這顯然會增加代碼的長度。

利用ogrid的傳回值,我能很容易計算x, y網格面上各點的值,或者x, y, z網格體上各點的值。下面是繪制三維曲面 x * exp(x**2 - y**2) 的程式:

NumPy的詳細教程 2 NumPy-快速處理資料

圖2.5 使用ogrid建立的三維曲面

NumPy的詳細教程 2 NumPy-快速處理資料

ufunc函數本身還有些方法,這些方法隻對兩個輸入一個輸出的ufunc函數有效,其它的ufunc對象調用這些方法時會抛出valueerror異常。

reduce 方法和python的reduce函數類似,它沿着axis軸對array進行操作,相當于将&lt;op&gt;運算符插入到沿axis軸的所有子數組或者元素當中。

例如:

accumulate 方法和reduce方法類似,隻是它傳回的數組和輸入的數組的shape相同,儲存所有的中間計算結果:

reduceat 方法計算多組reduce的結果,通過indices參數指定一系列reduce的起始和終了位置。reduceat的計算有些特别,讓我們通過一個例子來解釋一下:

對于indices中的每個元素都會調用reduce函數計算出一個值來,是以最終計算結果的長度和indices的長度相同。結果result數組中除最後一個元素之外,都按照如下計算得出:

而最後一個元素如下計算:

是以上面例子中,結果的每個元素如下計算而得:

可以看出result[::2]和a相等,而result[1::2]和np.add.accumulate(a)相等。

outer 方法,&lt;op&gt;.outer(a,b)方法的計算等同于如下程式:

其中squeeze的功能是剔除數組a中長度為1的軸。如果你看不太明白這個等同程式的話,讓我們來看一個例子:

可以看出通過outer方法計算的結果是如下的乘法表:

如果将這兩個數組按照等同程式一步一步的計算的話,就會發現乘法表最終是通過廣播的方式計算出來的。

NumPy的詳細教程 2 NumPy-快速處理資料

numpy和matlab不一樣,對于多元數組的運算,預設情況下并不使用矩陣運算,如果你希望對數組進行矩陣運算的話,可以調用相應的函數。

matrix對象

numpy庫提供了matrix類,使用matrix類建立的是矩陣對象,它們的加減乘除運算預設采用矩陣方式計算,是以用法和matlab十分類似。但是由于numpy中同時存在ndarray和matrix對象,是以使用者很容易将兩者弄混。這有違python的“顯式優于隐式”的原則,是以并不推薦在較複雜的程式中使用matrix。下面是使用matrix的一個例子:

因為a是用matrix建立的矩陣對象,是以乘法和幂運算符都變成了矩陣運算,于是上面計算的是矩陣a和其逆矩陣的乘積,結果是一個機關矩陣。

矩陣的乘積可以使用dot函數進行計算。對于二維數組,它計算的是矩陣乘積,對于一維數組,它計算的是其點積。當需要将一維數組當作列矢量或者行矢量進行矩陣運算時,推薦先使用reshape函數将一維數組轉換為二維數組:

除了dot計算乘積之外,numpy還提供了inner和outer等多種計算乘積的函數。這些函數計算乘積的方式不同,尤其是當對于多元數組的時候,更容易搞混。

dot : 對于兩個一維的數組,計算的是這兩個數組對應下标元素的乘積和(數學上稱之為内積);對于二維數組,計算的是兩個數組的矩陣乘積;對于多元數組,它的通用計算公式如下,即結果數組中的每個元素都是:數組a的最後一維上的所有元素與數組b的倒數第二位上的所有元素的乘積和:

下面以兩個3為數組的乘積示範一下dot乘積的計算結果: 首先建立兩個3維數組,這兩個數組的最後兩維滿足矩陣乘積的條件:

dot乘積的結果c可以看作是數組a,b的多個子矩陣的乘積:

inner : 和dot乘積一樣,對于兩個一維數組,計算的是這兩個數組對應下标元素的乘積和;對于多元數組,它計算的結果數組中的每個元素都是:數組a和b的最後一維的内積,是以數組a和b的最後一維的長度必須相同:

下面是inner乘積的示範:

outer : 隻按照一維數組進行計算,如果傳入參數是多元數組,則先将此數組展平為一維數組之後再進行運算。outer乘積計算的列向量和行向量的矩陣乘積:

矩陣中更進階的一些運算可以在numpy的線性代數子庫linalg中找到。例如inv函數計算逆矩陣,solve函數可以求解多元一次方程組。下面是solve函數的一個例子:

solve函數有兩個參數a和b。a是一個n*n的二維數組,而b是一個長度為n的一維數組,solve函數找到一個長度為n的一維數組x,使得a和x的矩陣乘積正好等于b,數組x就是多元一次方程組的解。

有關線性代數方面的内容将在今後的章節中詳細介紹。

NumPy的詳細教程 2 NumPy-快速處理資料

numpy提供了多種檔案操作函數友善我們存取數組内容。檔案存取的格式分為兩類:二進制和文本。而二進制格式的檔案又分為numpy專用的格式化二進制類型和無格式類型。

使用數組的方法函數tofile可以友善地将數組中資料以二進制的格式寫進檔案。tofile輸出的資料沒有格式,是以用numpy.fromfile讀回來的時候需要自己格式化資料:

從上面的例子可以看出,需要在讀入的時候設定正确的dtype和shape才能保證資料一緻。并且tofile函數不管數組的排列順序是c語言格式的還是fortran語言格式的,統一使用c語言格式輸出。

此外如果fromfile和tofile函數調用時指定了sep關鍵字參數的話,數組将以文本格式輸入輸出。

numpy.load和numpy.save函數以numpy專用的二進制類型儲存資料,這兩個函數會自動處理元素類型和shape等資訊,使用它們讀寫數組就友善多了,但是numpy.save輸出的檔案很難和其它語言編寫的程式讀入:

如果你想将多個數組儲存到一個檔案中的話,可以使用numpy.savez函數。savez函數的第一個參數是檔案名,其後的參數都是需要儲存的數組,也可以使用關鍵字參數為數組起一個名字,非關鍵字參數傳遞的數組會自動起名為arr_0, arr_1, ...。savez函數輸出的是一個壓縮檔案(擴充名為npz),其中每個檔案都是一個save函數儲存的npy檔案,檔案名對應于數組名。load函數自動識别npz檔案,并且傳回一個類似于字典的對象,可以通過數組名作為關鍵字擷取數組的内容:

如果你用解壓軟體打開result.npz檔案的話,會發現其中有三個檔案:arr_0.npy, arr_1.npy, sin_array.npy,其中分别儲存着數組a, b, c的内容。

使用numpy.savetxt和numpy.loadtxt可以讀寫1維和2維的數組:

檔案名和檔案對象

本節介紹所舉的例子都是傳遞的檔案名,也可以傳遞已經打開的檔案對象,例如對于load和save函數來說,如果使用檔案對象的話,可以将多個數組儲存到一個npy檔案中: