【Task02】Numpy學習打卡

Xiao_Spring發表於2020-10-23

六、副本與檢視

前言

在學習本章之前,我們先回顧一下Python中引用和物件的概念:

Python 中,一切皆物件。每個物件由:標識(identity)、型別(type)、value(值)
組成。

  • 標識用於唯一標識物件,通常對應於物件在計算機記憶體中的地址。使用內建函式 id(obj) 可返回物件 obj 的標識。
  • 型別用於表示物件儲存的“資料”的型別。型別可以限制物件的取值範圍以及可執行的 操作。可以使用 type(obj)獲得物件的所屬型別。
  • 值表示物件所儲存的資料的資訊。使用 print(obj)可以直接列印出值。

舉例說明:

【例六、0-1】python原生的物件和引用

#自定義輸出函式printInfo
>>> def printInfo(obj):
>>>     print("id:%s type:%s"%(id(obj),type(obj)))
>>> a = 2
>>> b = "datawhale"
>>> print("a的資訊",end='')
>>> printInfo(a)
a的資訊id:4503882768 type:<class 'int'>
>>> print("b的資訊",end='')
>>> printInfo(b)
b的資訊id:4571412400 type:<class 'str'>

在這裡插入圖片描述
如上圖所示,右邊分別是兩個不同型別的物件,變數a和b分別引用了這兩個物件。在python中,變數也成為:物件的引用。變數儲存的就是物件的地址。變數通過地址引用了“物件”。

【例六、0-2】python改變變數的引用

>>> data = [x for x in range(5)]
>>> data2 = [x for x in range(5,10)]
>>> print(data,data2)
[0, 1, 2, 3, 4] [5, 6, 7, 8, 9]
>>> a = data
>>> print(a,id(a),data,id(data))
[0, 1, 2, 3, 4] 4570869056 [0, 1, 2, 3, 4] 4570869056
>>> a[0] = 100
>>> print(a,id(a),data,id(data))
[100, 1, 2, 3, 4] 4570869056 [100, 1, 2, 3, 4] 4570869056
>>> a = data2
>>> print(a,id(a),data,id(data),data2,id(data2))
[5, 6, 7, 8, 9] 4654437248 [100, 1, 2, 3, 4] 4570869056 [5, 6, 7, 8, 9] 4654437248

在這裡插入圖片描述
如上圖,我們可以看到這樣的過程,開始時,我們可以通過變數a修改data的資料,變數a的引用從data轉向data2後,a的id和資料都發生了改變,與data2保持一致。

1.numpy中的引用

類比前言中python的引用例子,我們也可以看一下對numpy的資料型別ndarray進行引用會發生什麼:

【六、例1】引用並修改ndarray變數

>>> data = np.arange(5)
>>> a = data
>>> print(a,id(a),data,id(data))
>>> print(a is data)
[0 1 2 3 4] 4659785120 [0 1 2 3 4] 4659785120
True
>>> a[0] = 100
>>> print(a,id(a),data,id(data))
>>> print(a is data)
True

a is data也可以用來判斷2個變數所引用的物件id是否相同,id也叫做同一性運算子。

我們通過上面的例子可以看出,目前為主,numpy和python原生沒有太大區別。

2.檢視

檢視是資料的一個別稱或引用,通過該別稱或引用亦便可訪問、操作原有資料,但原有資料不會產生拷貝。如果我們對檢視進行修改,它會影響到原始資料,實體記憶體在同一位置。

檢視一般發生在:

  • numpy 的切片操作返回原資料的檢視。
  • 呼叫 ndarray 的 view() 函式產生一個檢視。

【六、例2-1】使用numpy切片操作返回檢視

#自定義輸出函式printInfo
>>> def printInfo(obj):
>>>     print(obj,end=" ")
>>>     print("id:%s type:%s"%(id(obj),type(obj)))

>>> data = np.arange(5)
>>> print("data:",end='')
>>> printInfo(data)
data:[0 1 2 3 4] id:4656357504 type:<class 'numpy.ndarray'>
>>> a = data[:2]
>>> print("a:",end=' ')
>>> printInfo(a)
a: [0 1] id:4661896176 type:<class 'numpy.ndarray'>
>>> b = data[:-1]
>>> print("b:",end=' ')
>>> printInfo(b)
b: [0 1 2 3] id:4661897776 type:<class 'numpy.ndarray'>
>>> a[0] = 100
>>> b[-1] = 200
>>> print(data)
[100   1   2 200   4]

>>> a.shape = (2,1)
>>> print(a)
>>> print(data)

[[100]
 [  1]]
[100   1   2 200   4]

a和b均由ndarray資料型別切片而來,可以看到,雖然data、a、b三個變數所引用的id不同,但是改變a和b的行為均在data上體現,但是改變a的shape並不會對data的shape產生影響。

【六、例2-2】呼叫 ndarray 的 view() 函式產生檢視

#自定義輸出函式printInfo
>>> def printInfo(obj):
>>>     print(obj,end=" ")
>>>     print("id:%s type:%s"%(id(obj),type(obj)))
    
>>> data = np.arange(5)
>>> a = data.view()
>>> print("data:",end='')
>>> printInfo(data)
data:[0 1 2 3 4] id:4679609280 type:<class 'numpy.ndarray'>
>>> print("a:",end=' ')
>>> printInfo(a)
a: [0 1 2 3 4] id:4661896976 type:<class 'numpy.ndarray'>
>>> a[0] = 100
>>> print(data)
[100   1   2   3   4]

>>> a.shape = (5,1)
>>> print(a)
>>> print(data)
[[100]
 [  1]
 [  2]
 [  3]
 [  4]]
[100   1   2   3   4]

通過view()方法生成檢視,變數a和變數data的id仍不相同,但是改變a仍然可以改變data,另外,用這種檢視生成方式改變a的shape,data的shape仍然不會改變。

那麼可以把檢視理解成半相關,部分相關(資料),部分無關(shape等)。

3、副本

副本是一個資料的完整的拷貝,如果我們對副本進行修改,它不會影響到原始資料,實體記憶體不在同一位置。

副本一般發生在:

  • Python 序列的切片操作,呼叫deepCopy()函式。
  • 呼叫 ndarray 的 copy() 函式產生一個副本。

【例六、3-1】Python 序列的切片生成副本

>>> a = [x for x in range(5)]
>>> b = a[:-1]
>>> print(a is b)
False
>>> print(a,b)
[0, 1, 2, 3, 4] [0, 1, 2, 3]
>>> b[0] = 100
>>> print(a,b)
[0, 1, 2, 3, 4] [100, 1, 2, 3]

可以看到通過python List的切片操作,生成的b和a的id不同,改變b中的資料也並不會改變a的資料。

【例六、3-2】呼叫 ndarray 的 copy() 函式生成副本

>>> a = np.arange(5)
>>> b = a.copy()
>>> print(a is b)
False
>>> print(a,b)
[0 1 2 3 4] [0 1 2 3 4]
>>> b[0] = 100
>>> print(a,b)
[0 1 2 3 4] [100   1   2   3   4]

numpy.ndarray.copy() 函式建立一個副本。 對副本資料進行修改,不會影響到原始資料,它們實體記憶體不在同一位置。

七、索引與切片

前言

ndarray物件的內容可以通過索引或切片來訪問和修改,與 Python 中 list 的切片操作一樣。

1.整數索引

【七、例1】通過整數索引訪問陣列

>>> a = np.random.randint(0,10,5)
>>> print(a,a[2])
[2 7 3 6 6] 3
>>> b = np.random.randint(0,10,(5,5))
>>> print(b,b[3][1],b[3,1])
[[3 7 0 7 9]
 [1 6 9 3 1]
 [2 7 5 0 7]
 [6 2 6 1 0]
 [5 5 6 8 1]] 2 2
>>> c = np.random.randint(0,10,(3,3,3))
>>> print(c,c[0][0][0],c[0,0,0])
[[[6 1 5]
  [9 5 9]
  [2 3 5]]

 [[1 1 5]
  [2 1 5]
  [1 5 4]]

 [[3 5 9]
  [8 9 6]
  [7 3 3]]] 6 6

這裡用了np.random.randint()方法生成了隨機ndarray陣列,然後利用整數索引進行訪問。

注:[x][y][z] 與 [x,y,z]的效果相同

在這裡插入圖片描述
顧名思義,我們可以把切片理解成一系列切蛋糕的行為,第一個引數是第一刀,第二個引數是最後一刀後面的位置,第三個引數是刀與刀之間的距離,那麼整數索引在二維陣列就相當於與橫縱都僅切1刀,且刀寬為1。

2.切片索引

我們在上一章知道,python切片獲得原來資料的副本(深拷貝),而ndarray資料型別切片獲得原資料的檢視。

ndarray 陣列可以基於 0 - n 的下標進行索引,切片物件可以通過內建的 slice 函式,並設定 start, stop 及 step 引數進行,從原陣列中切割出一個新陣列,也可以通過冒號分隔切片引數 start:stop:step 來進行切片操作。

【七、例2-1】單維度切片

#一維陣列
>>> a = np.random.randint(0,10,5)
>>> print(a)
>>> print(a[:])
>>> print(a[:-2])
>>> print(a[1:])
>>> print(a[::2])
[8 6 4 8 6]
[8 6 4 8 6]
[8 6 4]
[6 4 8 6]
[8 4 6]

#二維陣列
>>> a = np.random.randint(0,10,(5,5))
>>> print(a)
[[1 7 5 8 3]
 [1 7 8 7 3]
 [2 1 7 3 7]
 [3 3 4 1 4]
 [6 4 3 8 6]]
#列印倒數前2行之前的陣列(不包括倒數第二行)
>>> print(a[:-2])
[[1 7 5 8 3]
 [1 7 8 7 3]
 [2 1 7 3 7]]
#步長為2列印整個陣列
>>> print(a[::2])
[[1 7 5 8 3]
 [2 1 7 3 7]
 [6 4 3 8 6]]
#列印倒數前2列之前的陣列(不包括倒數第二列)
>>> print(a[...,:-2])
[[1 7 5]
 [1 7 8]
 [2 1 7]
 [3 3 4]
 [6 4 3]]
#列印倒數前2行之前的陣列(不包括倒數第二行)
>>> print(a[:-2,...])
[[1 7 5 8 3]
 [1 7 8 7 3]
 [2 1 7 3 7]]

二維陣列單維度切片示意圖如下:

上面的案例僅以一維、二維陣列為例進行單維度切片(在二維陣列中要麼是行,要麼是列),上升到多維陣列後,行列就要換成第幾維去描述。

我們注意到a[:-2]其實是和a[:-2,...]是等價的,那麼就此引出dots:

NumPy 允許使用...表示足夠多的冒號來構建完整的索引列表。

那麼在上述案例中,:::其實就和...是等價。對於其他維度的陣列,...可以起到一個補全的效果,我們接下來會在多維度切片中進行演示:

【七、例2-2】多維度切片

#二維陣列
>>> a = np.random.randint(0,10,(5,5))
>>> print(a)
[[7 7 9 8 0]
 [7 9 9 9 9]
 [2 1 4 7 7]
 [4 9 7 3 8]
 [2 1 8 2 8]]
#第2行與第3、4列交匯的資料
>>> print(a[1,2:4])
[9 9]
#第2列與第3、4行交匯的資料
>>> print(a[2:4,1])
[1 9]
#第3、4行與第3、4列交匯的資料
>>> print(a[2:4,2:4])
[[4 7]
 [7 3]]
#從第一行開始以2為步長的行與從第一行開始以2為步長的列交匯的資料
>>> print(a[::2,::2])
[[7 9 0]
 [2 4 7]
 [2 8 8]]
#去掉最後一列
>>> print(a[...,:-1])
[[7 7 9 8]
 [7 9 9 9]
 [2 1 4 7]
 [4 9 7 3]
 [2 1 8 2]]

二維陣列多維度切片示意圖:
在這裡插入圖片描述
複雜版:
在這裡插入圖片描述

在這裡比較好奇,如果是一行一列,返回的結果是怎樣的呢?

>>> print(a[0,0])
7

是一個整數,切片切多了,忘了[0,0]其實就是整數索引 —_-!

3.整數陣列索引

先簡單看一個案例:

【七、例3-1】單維度整數陣列索引

>>> a = np.random.randint(0,10,(5,5))
>>> print(a)
[[0 3 2 8 0]
 [2 6 8 7 9]
 [4 0 5 0 5]
 [8 5 9 0 0]
 [1 9 8 8 5]]
>>> s = [0,2,4]
#取a的第1,3,5行
>>> print(a[s])
[[0 3 2 8 0]
 [4 0 5 0 5]
 [1 9 8 8 5]]

整數陣列解決的是不連續切片且間隔是隨機非等差的問題,上面的案例是操作行,我們也可以同時操作行和列:

【七、例3-2】多維度整數陣列索引

>>> a = np.random.randint(0,10,(5,5))
>>> print(a)
[[6 2 9 7 2]
 [8 5 4 5 9]
 [1 4 3 3 9]
 [9 8 0 8 8]
 [7 1 6 5 2]]
>>> s = [0,2,3]
>>> p = [1,2,4]
>>> q = [1,2]
>>> print(a[s,p])
[2 3 8]
>>> print(a[s,q])
IndexError: shape mismatch: indexing arrays could not be broadcast together with shapes (3,) (2,)

最後一句程式碼出現問題,原因是傳入的兩個index陣列的數量必須相等,怎樣理解,就是將整數索引重複n遍,那麼兩個index陣列當然要相等。

二維陣列多維度整數陣列切片示意圖:
在這裡插入圖片描述

4.布林索引

與其稱為布林索引,不如理解成條件索引:

【七、例4-1】控制最小值及型別的布林索引

#控制最小值
>>> a = np.random.randint(0,10,(5,5))
>>> print(a)
[[5 9 6 6 1]
 [0 0 4 0 0]
 [0 8 2 5 7]
 [8 0 2 0 0]
 [3 4 0 1 6]]
>>> b = a > 5
>>> print(b)
[[False  True  True  True False]
 [False False False False False]
 [False  True False False  True]
 [ True False False False False]
 [False False False False  True]]
>>> print(a[b])
[9 6 6 8 7 8 6]

#控制型別,找到不是nan的資料
>>> c = np.array([np.nan,9,8,7,np.nan,6,5])
>>> d = np.logical_not(np.isnan(c))
>>> print(d)
[False  True  True  True False  True  True]
>>> print(c[d])
[9. 8. 7. 6. 5.]

我們其實可以看出,最終帶入的就是一個布林陣列b,然後利用a[b]去生成滿足條件的陣列。

【七、例4-2】布林索引在matplotlib上的應用

未完待續

八、陣列迭代

前言

除了for迴圈,Numpy 還提供另外一種更為優雅的遍歷方法。

  • apply_along_axis(func1d, axis, arr) Apply a function to 1-D slices along the given axis.

第一個引數是要執行的函式,第二個引數是遍歷的維度,第三個引數是要遍歷的資料

1.採用系統函式迭代

>>> a = np.random.randint(0,10,(5,5))
>>> print(a)
[[6 8 7 0 5]
 [3 4 4 3 7]
 [6 0 4 2 4]
 [3 9 8 4 8]
 [8 9 3 5 7]]
>>> b = np.apply_along_axis(np.sum, 0, x)
>>> print(b)
[25 12 28 12 28]
>>> b = np.apply_along_axis(np.sum, 1, x)
>>> print(b)
[28 19 16 24 18]

這裡解釋一下axis = 0的含義,以二維陣列距離,當axis = 0時,是指遍歷的方向是行,也就是說列是固定的,比如第一列6+3+6+3+8=25,就是結果陣列的第一個數,以此類推。axis = 1的含義是,遍歷的方向是列,示意圖如下:

在這裡插入圖片描述

2.採用自定義函式迭代

也就是更改第一個引數,如下:

>>> def xiao_func(x):
>>>     return (x[0] + x[3])
>>> b = np.apply_along_axis(xiao_func, 0, a)
>>> print(b)
[14  8 10  8 12]

相關文章