1. 程式人生 > >資料基礎---《利用Python進行資料分析·第2版》第4章 NumPy基礎:陣列和向量計算

資料基礎---《利用Python進行資料分析·第2版》第4章 NumPy基礎:陣列和向量計算

之前自己對於numpy和pandas是要用的時候東學一點西一點,直到看到《利用Python進行資料分析·第2版》,覺得只看這一篇就夠了。非常感謝原博主的翻譯和分享。

NumPy(Numerical Python的簡稱)是Python數值計算最重要的基礎包。大多數提供科學計算的包都是用NumPy的陣列作為構建基礎。

NumPy的部分功能如下:

  • ndarray,一個具有向量算術運算和複雜廣播能力的快速且節省空間的多維陣列。
  • 用於對整組資料進行快速運算的標準數學函式(無需編寫迴圈)。
  • 用於讀寫磁碟資料的工具以及用於操作記憶體對映檔案的工具。
  • 線性代數、隨機數生成以及傅立葉變換功能。
  • 用於整合由C、C++、Fortran等語言編寫的程式碼的A C API。

由於NumPy提供了一個簡單易用的C API,因此很容易將資料傳遞給由低階語言編寫的外部庫,外部庫也能以NumPy陣列的形式將資料返回給Python。這個功能使Python成為一種包裝C/C++/Fortran歷史程式碼庫的選擇,並使被包裝庫擁有一個動態的、易用的介面。

NumPy本身並沒有提供多麼高階的資料分析功能,理解NumPy陣列以及面向陣列的計算將有助於你更加高效地使用諸如pandas之類的工具。因為NumPy是一個很大的題目,我會在附錄A中介紹更多NumPy高階功能,比如廣播。

對於大部分資料分析應用而言,我最關注的功能主要集中在:

  • 用於資料整理和清理、子集構造和過濾、轉換等快速的向量化陣列運算。
  • 常用的陣列演算法,如排序、唯一化、集合運算等。
  • 高效的描述統計和資料聚合/摘要運算。
  • 用於異構資料集的合併/連線運算的資料對齊和關係型資料運算。
  • 將條件邏輯表述為陣列表示式(而不是帶有if-elif-else分支的迴圈)。
  • 資料的分組運算(聚合、轉換、函式應用等)。。

雖然NumPy提供了通用的數值資料處理的計算基礎,但大多數讀者可能還是想將pandas作為統計和分析工作的基礎,尤其是處理表格資料時。pandas還提供了一些NumPy所沒有的領域特定的功能,如時間序列處理等。

筆記:Python的面向陣列計算可以追溯到1995年,Jim Hugunin建立了Numeric庫。接下來的10年,許多科學程式設計社群紛紛開始使用Python的陣列程式設計,但是進入21世紀,庫的生態系統變得碎片化了。2005年,Travis Oliphant從Numeric和Numarray專案整了出了NumPy專案,進而所有社群都集合到了這個框架下。

NumPy之於數值計算特別重要的原因之一,是因為它可以高效處理大陣列的資料。這是因為:

  • NumPy是在一個連續的記憶體塊中儲存資料,獨立於其他Python內建物件。NumPy的C語言編寫的演算法庫可以操作記憶體,而不必進行型別檢查或其它前期工作。比起Python的內建序列,NumPy陣列使用的記憶體更少。
  • NumPy可以在整個陣列上執行復雜的計算,而不需要Python的for迴圈。

要搞明白具體的效能差距,考察一個包含一百萬整數的陣列,和一個等價的Python列表:

import numpy as np
my_arr=np.arange(1000000)
my_list=list(range(1000000))
%time for _ in range(10):my_arr2=my_arr*2
Wall time: 22.9 ms
%time for _ in range(10):my_list2=[x*2 for x in my_list]
Wall time: 912 ms

基於NumPy的演算法要比純Python快10到100倍(甚至更快),並且使用的記憶體更少。

4.1 NumPy的ndarray:一種多維陣列物件

NumPy最重要的一個特點就是其N維陣列物件(即ndarray),該物件是一個快速而靈活的大資料集容器。你可以利用這種陣列對整塊資料執行一些數學運算,其語法跟標量元素之間的運算一樣。

要明白Python是如何利用與標量值類似的語法進行批次計算,我先引入NumPy,然後生成一個包含隨機資料的小陣列:

# Generate some random data
data=np.random.randn(2,3)
data
array([[-2.13621957, -0.47543594, -0.31882647],
       [ 0.79032757, -0.39074584, -0.85108502]])

然後進行數學運算:

data*10
array([[-21.36219572,  -4.75435944,  -3.18826471],
       [  7.90327566,  -3.90745843,  -8.51085021]])
data+data
array([[-4.27243914, -0.95087189, -0.63765294],
       [ 1.58065513, -0.78149169, -1.70217004]])

第一個例子中,所有的元素都乘以10。第二個例子中,每個元素都與自身相加。

筆記:在本章及全書中,我會使用標準的NumPy慣用法import numpy as np。你當然也可以在程式碼中使用from numpy import *,但不建議這麼做。numpy的名稱空間很大,包含許多函式,其中一些的名字與Python的內建函式重名(比如min和max)。

ndarray是一個通用的同構資料多維容器,也就是說,其中的所有元素必須是相同型別的。每個陣列都有一個shape(一個表示各維度大小的元組)和一個dtype(一個用於說明陣列資料型別的物件):

data.shape
(2, 3)
data.dtype
dtype('float64')

本章將會介紹NumPy陣列的基本用法,這對於本書後面各章的理解基本夠用。雖然大多數資料分析工作不需要深入理解NumPy,但是精通面向陣列的程式設計和思維方式是成為Python科學計算牛人的一大關鍵步驟。

筆記:當你在本書中看到“陣列”、“NumPy陣列”、"ndarray"時,基本上都指的是同一樣東西,即ndarray物件。

建立ndarray

建立陣列最簡單的辦法就是使用array函式。它接受一切序列型的物件(包括其他陣列),然後產生一個新的含有傳入資料的NumPy陣列。以一個列表的轉換為例:

data1 = [6, 7.5, 8, 0, 1]
arr1=np.array(data1)
arr1
array([6. , 7.5, 8. , 0. , 1. ])

巢狀序列(比如由一組等長列表組成的列表)將會被轉換為一個多維陣列:

data2 = [[1, 2, 3, 4], [5, 6, 7, 8]]
arr2=np.array(data2)
arr2
array([[1, 2, 3, 4],
       [5, 6, 7, 8]])

因為data2是列表的列表,NumPy陣列arr2的兩個維度的shape是從data2引入的。可以用屬性ndim和shape驗證:

arr2.ndim
2
arr2.shape
(2, 4)
data3=[[[1, 2, 3, 4], [5, 6, 7, 8]],[[1, 2, 3, 4], [5, 6, 7, 8]]]
arr3=np.array(data3)
arr3
array([[[1, 2, 3, 4],
        [5, 6, 7, 8]],

       [[1, 2, 3, 4],
        [5, 6, 7, 8]]])
arr3.ndim
3
arr3.shape#先最外層有幾行,再看內層有幾行,再看內層有幾列
(2, 2, 4)

除非特別說明(稍後將會詳細介紹),np.array會嘗試為新建的這個陣列推斷出一個較為合適的資料型別(所以pandas也會自動推斷)。資料型別儲存在一個特殊的dtype物件中。比如說,在上面的兩個例子中,我們有:

arr1.dtype
dtype('float64')
arr2.dtype
dtype('int32')

除np.array之外,還有一些函式也可以新建陣列。比如,zeros和ones分別可以建立指定長度或形狀的全0或全1陣列。empty可以建立一個沒有任何具體值的陣列。要用這些方法建立多維陣列,只需傳入一個表示形狀的元組即可:

np.zeros(10)
array([0., 0., 0., 0., 0., 0., 0., 0., 0., 0.])
np.zeros((3,6))
array([[0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 0., 0.]])
np.empty((2,3,2))
array([[[0., 0.],
        [0., 0.],
        [0., 0.]],

       [[0., 0.],
        [0., 0.],
        [0., 0.]]])

注意:認為np.empty會返回全0陣列的想法是不安全的。很多情況下(如前所示),它返回的都是一些未初始化的垃圾值。

arange是Python內建函式range的陣列版:

np.arange(15)
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14])

表4-1列出了一些陣列建立函式。由於NumPy關注的是數值計算,因此,如果沒有特別指定,資料型別基本都是float64(浮點數)。

函式 說明
array 將輸入資料(列表、元組、陣列或其它序列型別)轉換為 ndarray.要麼推斷出dtype,要麼持別指定 dtype,預設直按複製輸入資料
asarray 將輸入轉換為 ndarray如果輸入本身就是一個 naray就不進行復制
arange 類似於內建的 range,但返回的是一個 ndarray而不是列表
ones, ones_like 根據指定的形狀和dtype建立一個全1陣列, ones_like以另一個數組為引數,並根據其形狀和dtype建立一個全1陣列
zeros, zeros_like 類似於ones和ones_like,只不過產生的是全0陣列而已
empty, empty_like 建立新陣列,只分配記憶體空間但不填充任何值
full,full_like 用 fill value中的所有值,根據指定的形狀和dtype建立一個數組,full_like使用另一個數組,用相同的形狀和dtype建立
eye, identity 建立一個正方的NxN單位矩陣(對角線為1,其餘為0)
表4-1 陣列建立函式

ndarray的資料型別

dtype(資料型別)是一個特殊的物件,它含有ndarray將一塊記憶體解釋為特定資料型別所需的資訊:

arr1=np.array([1,2,3],dtype=np.float64)
arr2=np.array([1,2,3],dtype=np.int32)
arr1.dtype
dtype('float64')
arr2.dtype
dtype('int32')

dtype是NumPy靈活互動其它系統的源泉之一。多數情況下,它們直接對映到相應的機器表示,這使得“讀寫磁碟上的二進位制資料流”以及“整合低階語言程式碼(如C、Fortran)”等工作變得更加簡單。數值型dtype的命名方式相同:一個型別名(如float或int),後面跟一個用於表示各元素位長的數字。標準的雙精度浮點值(即Python中的float物件)需要佔用8位元組(即64位)。因此,該型別在NumPy中就記作float64。表4-2列出了NumPy所支援的全部資料型別。

筆記:記不住這些NumPy的dtype也沒關係,新手更是如此。通常只需要知道你所處理的資料的大致型別是浮點數、複數、整數、布林值、字串,還是普通的Python物件即可。當你需要控制資料在記憶體和磁碟中的儲存方式時(尤其是對大資料集),那就得了解如何控制儲存型別。

你可以通過ndarray的astype方法明確地將一個數組從一個dtype轉換成另一個dtype:

arr=np.array([1, 2, 3, 4, 5])
arr.dtype
dtype('int32')
float_arr = arr.astype(np.float64)
float_arr.dtype
dtype('float64')

在本例中,整數被轉換成了浮點數。如果將浮點數轉換成整數,則小數部分將會被擷取刪除:

arr=np.array([3.7, -1.2, -2.6, 0.5, 12.9, 10.1])
arr
array([ 3.7, -1.2, -2.6,  0.5, 12.9, 10.1])
arr.astype(np.int32)
array([ 3, -1, -2,  0, 12, 10])

如果某字串陣列表示的全是數字,也可以用astype將其轉換為數值形式:

np.array(['1.25', '-9.6', '42'])
array(['1.25', '-9.6', '42'], dtype='<U4')
numeric_strings = np.array(['1.25', '-9.6', '42'], dtype=np.string_)
numeric_strings
array([b'1.25', b'-9.6', b'42'], dtype='|S4')
numeric_strings.dtype
dtype('S4')
numeric_strings.astype(np.float)
array([ 1.25, -9.6 , 42.  ])

注意:使用numpy.string_型別時,一定要小心,因為NumPy的字串資料是大小固定的,發生擷取時,不會發出警告。pandas提供了更多非數值資料的便利的處理方法。

如果轉換過程因為某種原因而失敗了(比如某個不能被轉換為float64的字串),就會引發一個ValueError。這裡,我比較懶,寫的是float而不是np.float64;NumPy很聰明,它會將Python型別對映到等價的dtype上。

陣列的dtype還有另一個屬性:

int_array =np.arange(10)
calibers = np.array([.22, .270, .357, .380, .44, .50], dtype=np.float64)
int_array.astype(calibers.dtype)
array([0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])
int_array.dtype
dtype('int32')

你還可以用簡潔的型別程式碼來表示dtype:

empty_uint32 = np.empty(8, dtype='u4')
empty_uint32
array([         0, 1075314688,          0, 1075707904,          0,
       1075838976,          0, 1072693248], dtype=uint32)

筆記:呼叫astype總會建立一個新的陣列(一個數據的備份),即使新的dtype與舊的dtype相同。

NumPy陣列的運算

陣列很重要,因為它使你不用編寫迴圈即可對資料執行批量運算。NumPy使用者稱其為向量化(vectorization)。大小相等的陣列之間的任何算術運算都會將運算應用到元素級:

arr=np.array([[1., 2., 3.], [4., 5., 6.]])
arr
array([[1., 2., 3.],
       [4., 5., 6.]])
arr*arr
array([[ 1.,  4.,  9.],
       [16., 25., 36.]])
arr-arr
array([[0., 0., 0.],
       [0., 0., 0.]])

陣列與標量的算術運算會將標量值傳播到各個元素:

1/arr
array([[1.        , 0.5       , 0.33333333],
       [0.25      , 0.2       , 0.16666667]])
arr**0.5
array([[1.        , 1.41421356, 1.73205081],
       [2.        , 2.23606798, 2.44948974]])

大小相同的陣列之間的比較會生成布林值陣列:

arr2 = np.array([[0., 4., 1.], [7., 2., 12.]])
arr2
array([[ 0.,  4.,  1.],
       [ 7.,  2., 12.]])
arr2>arr
array([[False,  True, False],
       [ True, False,  True]])

不同大小的陣列之間的運算叫做廣播(broadcasting),將在附錄A中對其進行詳細討論。本書的內容不需要對廣播機制有多深的理解。

基本的索引和切片

NumPy陣列的索引是一個內容豐富的主題,因為選取資料子集或單個元素的方式有很多。一維陣列很簡單。從表面上看,它們跟Python列表的功能差不多:

arr = np.arange(10)
arr
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
arr[5]
5
arr[5:8]
array([5, 6, 7])
arr[5:8]=12
arr
array([ 0,  1,  2,  3,  4, 12, 12, 12,  8,  9])

如上所示,當你將一個標量值賦值給一個切片時(如arr[5:8]=12),該值會自動傳播(也就說後面將會講到的“廣播”)到整個選區。跟列表最重要的區別在於,陣列切片是原始陣列的檢視。這意味著資料不會被複制,檢視上的任何修改都會直接反映到源陣列上。

a=list(range(5))
a
[0, 1, 2, 3, 4]
a[0]=4
a
[4, 1, 2, 3, 4]
b=a[1:3]
b
[1, 2]
b[0]=4
b
[4, 2]
a
[4, 1, 2, 3, 4]

作為例子,先建立一個arr的切片:

arr_slice = arr[5:8]
arr_slice
array([12, 12, 12])

現在,當我修稿arr_slice中的值,變動也會體現在原始陣列arr中:

arr_slice[1] = 12345
arr
array([    0,     1,     2,     3,     4,    12, 12345,    12,     8,
           9])

切片[ : ]會給陣列中的所有值賦值:

arr_slice[:]=64
arr
array([ 0,  1,  2,  3,  4, 64, 64, 64,  8,  9])

如果你剛開始接觸NumPy,可能會對此感到驚訝(尤其是當你曾經用過其他熱衷於複製陣列資料的程式語言)。由於NumPy的設計目的是處理大資料,所以你可以想象一下,假如NumPy堅持要將資料複製來複制去的話會產生何等的效能和記憶體問題。

注意:如果你想要得到的是ndarray切片的一份副本而非檢視,就需要明確地進行復制操作,例如arr[5:8].copy()。

對於高維度陣列,能做的事情更多。在一個二維陣列中,各索引位置上的元素不再是標量而是一維陣列:

arr2d=np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
arr2d
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])
arr2d[2]
array([7, 8, 9])
arr2d[:,2]
array([3, 6, 9])

因此,可以對各個元素進行遞迴訪問,但這樣需要做的事情有點多。你可以傳入一個以逗號隔開的索引列表來選取單個元素。也就是說,下面兩種方式是等價的:

arr2d[0][2]
3
arr2d[0,2]
3

圖4-1說明了二維陣列的索引方式。軸0作為行,軸1作為列。

圖4-1 NumPy陣列中的元素索引

在多維陣列中,如果省略了後面的索引,則返回物件會是一個維度低一點的ndarray(它含有高一級維度上的所有資料)。因此,在2×2×3陣列arr3d中:

arr3d = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]])
arr3d
array([[[ 1,  2,  3],
        [ 4,  5,  6]],

       [[ 7,  8,  9],
        [10, 11, 12]]])

arr3d[0]是一個2×3陣列:

arr3d[0]
array([[1, 2, 3],
       [4, 5, 6]])

標量值和陣列都可以被賦值給arr3d[0]:

old_values =arr3d[0].copy()
arr3d[0]=42
arr3d
array([[[42, 42, 42],
        [42, 42, 42]],

       [[ 7,  8,  9],
        [10, 11, 12]]])
arr3d[0] = old_values
arr3d
array([[[ 1,  2,  3],
        [ 4,  5,  6]],

       [[ 7,  8,  9],
        [10, 11, 12]]])

相似的,arr3d[1,0]可以訪問索引以(1,0)開頭的那些值(以一維陣列的形式返回):

arr3d[1, 0]
array([7, 8, 9])

雖然是用兩步進行索引的,表示式是相同的:

x = arr3d[1]
x
array([[ 7,  8,  9],
       [10, 11, 12]])
x[0]
array([7, 8, 9])

注意,在上面所有這些選取陣列子集的例子中,返回的陣列都是檢視。

切片索引

ndarray的切片語法跟Python列表這樣的一維物件差不多:

arr
array([ 0,  1,  2,  3,  4, 64, 64, 64,  8,  9])
arr[1:6]
array([ 1,  2,  3,  4, 64])

對於之前的二維陣列arr2d,其切片方式稍顯不同:

arr2d
array([[1, 2, 3],
       [4, 5, 6],
       [7, 8, 9]])
arr2d[:2]
array([[1, 2, 3],
       [4, 5, 6]])

可以看出,它是沿著第0軸(即第一個軸)切片的。也就是說,切片是沿著一個軸向選取元素的。表示式arr2d[:2]可以被認為是“選取arr2d的前兩行”。

你可以一次傳入多個切片,就像傳入多個索引那樣:

arr2d[:2, 1:]
array([[2, 3],
       [5, 6]])

像這樣進行切片時,只能得到相同維數的陣列檢視。通過將整數索引和切片混合,可以得到低維度的切片。

例如,我可以選取第二行的前兩列:

arr2d[1, :2]
array([4, 5])

相似的,還可以選擇第三列的前兩行:

arr2d[:2,2]
array([3, 6])

圖4-2對此進行了說明。注意,“只有冒號”表示選取整個軸,因此你可以像下面這樣只對高維軸進行切片:

arr2d[:, :1]
array([[1],
       [4],
       [7]])
圖4-2 二維陣列切片

自然,對切片表示式的賦值操作也會被擴散到整個選區:

arr2d[:2, 1:] = 0
arr2d
array([[1, 0, 0],
       [4, 0, 0],
       [7, 8, 9]])

布林型索引

來看這樣一個例子,假設我們有一個用於儲存資料的陣列以及一個儲存姓名的陣列(含有重複項)。在這裡,我將使用numpy.random中的randn函式生成一些正態分佈的隨機資料:

names=np.array(['Bob', 'Joe', 'Will', 'Bob', 'Will', 'Joe', 'Joe'])
data=np.random.randn(7,4)
data
array([[ 0.10540172,  0.89924365,  1.24212294, -0.57773063],
       [ 0.15738407,  0.91053318,  0.27987987, -1.33957695],
       [ 1.44582399,  0.72827796, -0.20991932,  0.15654456],
       [-2.91833355, -0.04922086, -0.77391411,  2.12123355],
       [ 0.78468854,  0.04009856, -0.28841656,  0.42606126],
       [-0.48360283, -0.22064174, -0.12985216,  1.30422498],
       [ 0.62969078,  0.57436525, -1.49846719, -0.59926296]])

假設每個名字都對應data陣列中的一行,而我們想要選出對應於名字"Bob"的所有行。跟算術運算一樣,陣列的比較運算(如==)也是向量化的。因此,對names和字串"Bob"的比較運算將會產生一個布林型陣列:

names=='Bob'
array([ True, False, False,  True, False, False, False])

這個布林型陣列可用於陣列索引:

data[names=='Bob']
array([[ 0.10540172,  0.89924365,  1.24212294, -0.57773063],
       [-2.91833355, -0.04922086, -0.77391411,  2.12123355]])

布林型陣列的長度必須跟被索引的軸長度一致。此外,還可以將布林型陣列跟切片、整數(或整數序列,稍後將對此進行詳細講解)混合使用:

注意:如果布林型陣列的長度不對,布林型選擇就會出錯,因此一定要小心。

下面的例子,我選取了names == 'Bob’的行,並索引了列:

data[names=='Bob',2:]
array([[ 1.24212294, -0.57773063],
       [-0.77391411,  2.12123355]])

要選擇除"Bob"以外的其他值,既可以使用不等於符號(!=),也可以通過~對條件進行否定:

names!='Bob'
array([False,  True,  True, False,  True,  True,  True])
data[~(names=='Bob')]
array([[ 0.15738407,  0.91053318,  0.27987987, -1.33957695],
       [ 1.44582399,  0.72827796, -0.20991932,  0.15654456],
       [ 0.78468854,  0.04009856, -0.28841656,  0.42606126],
       [-0.48360283, -0.22064174, -0.12985216,  1.30422498],
       [ 0.62969078,  0.57436525, -1.49846719, -0.59926296]])

~操作符用來反轉條件很好用:

cond = names == 'Bob'
data[~cond]
array([[ 0.15738407,  0.91053318,  0.27987987, -1.33957695],
       [ 1.44582399,  0.72827796, -0.20991932,  0.15654456],
       [ 0.78468854,  0.04009856, -0.28841656,  0.42606126],
       [-0.48360283, -0.22064174, -0.12985216,  1.30422498],
       [ 0.62969078,  0.57436525, -1.49846719, -0.59926296]])

選取這三個名字中的兩個需要組合應用多個布林條件,使用&(和)、|(或)之類的布林算術運算子即可:

mask=(names=='Bob')|(names=='Will')
mask
array([ True, False,  True,  True,  True, False, False])
data[mask]
array([[ 0.10540172,  0.89924365,  1.24212294, -0.57773063],
       [ 1.44582399,  0.72827796, -0.20991932,  0.15654456],
       [-2.91833355, -0.04922086, -0.77391411,  2.12123355],
       [ 0.78468854,  0.04009856, -0.28841656,  0.42606126]])

通過布林型索引選取陣列中的資料,將總是建立資料的副本,即使返回一模一樣的陣列也是如此。

注意:Python關鍵字and和or在布林型陣列中無效。要使用&與|。

通過布林型陣列設定值是一種經常用到的手段。為了將data中的所有負值都設定為0,我們只需:

data[data<0]=0
data
array([[0.10540172, 0.89924365, 1.24212294, 0.        ],
       [0.15738407, 0.91053318, 0.27987987, 0.        ],
       [1.44582399, 0.72827796, 0.        , 0.15654456],
       [0.        , 0.        , 0.        , 2.12123355],
       [0.78468854, 0.04009856, 0.        , 0.42606126],
       [0.        , 0.        , 0.        , 1.30422498],
       [0.62969078, 0.57436525, 0.        , 0.        ]])

通過一維布林陣列設定整行或列的值也很簡單:

data[names!='Joe']=7
data
array([[7.        , 7.        , 7.        , 7.        ],
       [0.15738407, 0.91053318, 0.27987987, 0.        ],
       [7.        , 7.        , 7.        , 7.        ],
       [7.        , 7.        , 7.        , 7.        ],
       [7.        , 7.        , 7.        , 7.        ],
       [0.        , 0.        , 0.        , 1.30422498],
       [0.62969078, 0.57436525, 0.        , 0.        ]])

後面會看到,這類二維資料的操作也可以用pandas方便的來做。pandas也是可以用bool索引,用起一樣

花式索引

花式索引(Fancy indexing)是一個NumPy術語,它指的是利用整數陣列進行索引。假設我們有一個8×4陣列:

arr=np.empty((8, 4))
arr
array([[6.23042070e-307, 1.86918699e-306, 1.69121096e-306,
        1.33511562e-306],
       [1.29060531e-306, 8.45599366e-307, 7.56593017e-307,
        1.33511290e-306],
       [1.42417221e-306, 1.37961641e-306, 6.23038675e-307,
        6.23053954e-307],
       [9.34609790e-307, 8.45593934e-307, 9.34600963e-307,
        1.86921143e-306],
       [6.23061763e-307, 1.78021527e-306, 6.23055651e-307,
        9.34609111e-307],
       [1.37962117e-306, 6.89804133e-307, 8.45610231e-307,
        9.34601642e-307],
       [1.33510679e-306, 1.78019625e-306, 9.34609790e-307,
        1.42418172e-306],
       [1.37961641e-306, 1.16820707e-307, 8.45610231e-307,
        0.00000000e+000]])
for i in range(8):
    arr[i]=i
arr
array([[0., 0., 0., 0.],
       [1., 1., 1., 1.],
       [2., 2., 2., 2.],
       [3., 3., 3., 3.],
       [4., 4., 4., 4.],
       [5., 5., 5., 5.],
       [6., 6., 6., 6.],
       [7., 7., 7., 7.]])

為了以特定順序選取行子集,只需傳入一個用於指定順序的整數列表或ndarray即可:

arr[[4, 3, 0, 6]]
array([[4., 4., 4., 4.],
       [3., 3., 3., 3.],
       [0., 0., 0., 0.],
       [6., 6., 6., 6.]])

這段程式碼確實達到我們的要求了!使用負數索引將會從末尾開始選取行:

arr[[-3, -5, -7]]
array([[5., 5., 5., 5.],
       [3., 3., 3., 3.],
       [1., 1., 1., 1.]])

一次傳入多個索引陣列會有一點特別。它返回的是一個一維陣列,其中的元素對應各個索引元組

arr=np.arange(32).reshape((8, 4))
arr
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11],
       [12, 13, 14, 15],
       [16, 17, 18, 19],
       [20, 21, 22, 23],
       [24, 25, 26, 27],
       [28, 29, 30, 31]])
arr[[1, 5, 7, 2], [0, 3, 1, 2]]
array([ 4, 23, 29, 10])
arr[[(1,0)]]
array([[4, 5, 6, 7],
       [0, 1, 2, 3]])

附錄A中會詳細介紹reshape方法。

最終選出的是元素(1,0)、(5,3)、(7,1)和(2,2)。無論陣列是多少維的,花式索引總是一維的。

這個花式索引的行為可能會跟某些使用者的預期不一樣(包括我在內,上面通過bool索引+列索引可以得到一個子區域,其實是切片;但這裡用行引的陣列+列索引陣列得到的是對應位置的元素,而不是子區域),選取矩陣的行列子集應該是矩形區域的形式才對。下面是得到該結果的一個辦法:先取出行,再取所有行某些列對應元素,從而實現取子區域的目的。

arr[[1, 5, 7, 2]][:,[0, 3, 1, 2]]
array([[ 4,  7,  5,  6],
       [20, 23, 21, 22],
       [28, 31, 29, 30],
       [ 8, 11,  9, 10]])

記住,花式索引跟切片不一樣,它總是將資料複製到新陣列中(形式上也不一樣,切片是用行列索引號取資料,而花式索引是用由索引號構成的陣列