資料科學 IPython 筆記本 9.4 NumPy 陣列的基礎
9.4 NumPy 陣列的基礎
本節是《Python 資料科學手冊》(Python Data Science Handbook)的摘錄。
譯者:飛龍
Python 中的資料操作幾乎與 NumPy 陣列操作同義:即使是像 Pandas 這樣的新工具也是圍繞 NumPy 陣列構建的。本節將介紹幾個示例,使用 NumPy 陣列操作來訪問資料和子陣列,以及拆分,重塑和連線陣列。
雖然這裡顯示的操作型別可能看起來有點枯燥和怪異,但它們構成了本書中使用的許多其他示例的積木。儘快瞭解它們!
我們將在這裡介紹幾類基本陣列操作:
- 陣列的屬性:確定陣列的大小,形狀,記憶體消耗和資料型別
- 陣列的索引:獲取和設定各個陣列元素的值
- 陣列切片:在較大的陣列中獲取和設定較小的子陣列
- 陣列的重塑:更改給定陣列的形狀
- 陣列的連線和分割:將多個數組合併為一個數組,並將一個數組拆分為多個數組
NumPy 陣列屬性
首先讓我們討論一些有用的陣列屬性。
我們首先定義三個隨機陣列,一維,二維和三維陣列。我們將使用 NumPy 的隨機數生成器,並使用設定值設定種子,來確保每次執行此程式碼時,生成相同的隨機陣列:
import numpy as np
np.random.seed(0) # 用於可復現的種子
x1 = np.random.randint(10, size=6) # 一維陣列
x2 = np.random.randint(10, size=(3, 4)) # 二維陣列
x3 = np.random.randint(10, size=(3, 4, 5)) # 三維陣列
每個陣列都有屬性ndim
(維數),shape
(每個維度的大小)和size
(陣列的總大小):
print("x3 ndim: ", x3.ndim)
print("x3 shape:", x3.shape)
print("x3 size: ", x3.size)
'''
x3 ndim: 3
x3 shape: (3, 4, 5)
x3 size: 60
'''
另一個有用的屬性是dtype
,陣列的資料型別(我們之前在“瞭解 Python 中的資料型別”中討論過):
print("dtype:", x3.dtype)
# dtype: int64
其他屬性包括itemsize
,它列出每個陣列元素的大小(以位元組為單位)和nbytes
,它列出了陣列的總大小(以位元組為單位):
print("itemsize:", x3.itemsize, "bytes")
print("nbytes:", x3.nbytes, "bytes")
'''
itemsize: 8 bytes
nbytes: 480 bytes
'''
一般來說,我們希望nbytes
等於itemsize
乘以size
。
陣列索引:訪問單個元素
如果你熟悉 Python 的標準列表索引,NumPy 中的索引將會非常眼熟。
在一維陣列中,可以通過在方括號中指定所需的索引(從零開始計算),來訪問第i
值,就像使用 Python 列表一樣:
x1
# array([5, 0, 3, 3, 7, 9])
x1[0]
# 5
x1[4]
# 7
要從陣列的末尾開始索引,可以使用負索引:
x1[-1]
# 9
x1[-2]
# 7
在多維陣列中,可以使用以逗號分隔的索引元組來訪問專案:
x2
'''
array([[3, 5, 2, 4],
[7, 6, 8, 8],
[1, 6, 7, 7]])
'''
x2[0, 0]
# 3
x2[2, 0]
# 1
x2[2, -1]
# 7
也可以使用以上任何索引表示法修改值:
x2[0, 0] = 12
x2
'''
array([[12, 5, 2, 4],
[ 7, 6, 8, 8],
[ 1, 6, 7, 7]])
'''
請記住,與 Python 列表不同,NumPy 陣列具有固定型別。
這意味著,例如,如果你嘗試將浮點值插入整數陣列,則該值將被靜默截斷。 不要意識不到這種行為!
x1[0] = 3.14159 # 會截斷!
x1
# array([3, 0, 3, 3, 7, 9])
陣列切片:訪問子陣列
就像我們可以使用方括號來訪問單個數組元素一樣,我們也可以使用它們以及由冒號(:
)標記的切片表示法,來訪問子陣列。
NumPy 切片語法遵循標準 Python 列表的語法;要訪問陣列x
的切片,請使用:
x[start:stop:step]
如果其中任何一個未指定,它們預設為start = 0
,stop = 維度大小
,step = 1
。
我們看一下如何在一維和多維中訪問子陣列。
一維子陣列
x = np.arange(10)
x
# array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
x[:5] # 前五個元素
# array([0, 1, 2, 3, 4])
x[5:] # 下標 5 後面的元素
# array([5, 6, 7, 8, 9])
x[4:7] # 中間的子陣列
# array([4, 5, 6])
x[::2] # 每個其它元素
# array([0, 2, 4, 6, 8])
x[1::2] # 每個其它元素,起始於下標 1
# array([1, 3, 5, 7, 9])
一個可能令人困惑的情況是step
值為負。在這種情況下,交換start
和stop
的預設值。這成為反轉陣列的便捷方法:
x[::-1] # 所有元素反過來
# array([9, 8, 7, 6, 5, 4, 3, 2, 1, 0])
x[5::-2] # 反向的每個其它元素,起始於下標 5
# array([5, 3, 1])
多維子陣列
多維切片以相同的方式工作,多個切片用逗號分隔。例如:
x2
'''
array([[12, 5, 2, 4],
[ 7, 6, 8, 8],
[ 1, 6, 7, 7]])
'''
x2[:2, :3] # 兩行三列
'''
array([[12, 5, 2],
[ 7, 6, 8]])
'''
x2[:3, ::2] # 所有行,每個其它列
'''
array([[12, 2],
[ 7, 8],
[ 1, 7]])
'''
最後,子陣列的維度甚至可以一起反轉:
x2[::-1, ::-1]
'''
array([[ 7, 7, 6, 1],
[ 8, 8, 6, 7],
[ 4, 2, 5, 12]])
'''
訪問陣列的行和列
一個常用的例程是訪問陣列的單個行或列。
這可以通過組合索引和切片來完成,使用由單個冒號(:
)標記的空切片:
print(x2[:, 0]) # x2 的第一列
# [12 7 1]
print(x2[0, :]) # x2 的第一行
# [12 5 2 4]
在訪問行的情況下,可以省略空切片來獲得更緊湊的語法:
print(x2[0]) # 等價於 x2[0, :]
# [12 5 2 4]
作為無副本檢視的子陣列
陣列切片的一個重要且非常有用的事情,是它們返回檢視而不是陣列資料的副本。這是 NumPy 陣列切片與 Python 列表切片的不同之處:在列表中,切片是副本。
考慮我們之前的二維陣列:
print(x2)
'''
[[12 5 2 4]
[ 7 6 8 8]
[ 1 6 7 7]]
'''
讓我們從中提取2x2
子陣列:
x2_sub = x2[:2, :2]
print(x2_sub)
'''
[[12 5]
[ 7 6]]
'''
現在,如果我們修改這個子陣列,我們會看到原始陣列已經改變了!注意:
x2_sub[0, 0] = 99
print(x2_sub)
'''
[[99 5]
[ 7 6]]
'''
print(x2)
'''
[[99 5 2 4]
[ 7 6 8 8]
[ 1 6 7 7]]
'''
這種預設行為實際上非常有用:這意味著當我們處理大型資料集時,我們可以訪問和處理這些資料集的各個部分,而無需複製底層資料緩衝區。
建立陣列的副本
儘管陣列檢視具有很好的特性,但有時顯式複製陣列或子陣列中的資料也很有用。 使用copy()
方法可以很容易地做到:
x2_sub_copy = x2[:2, :2].copy()
print(x2_sub_copy)
'''
[[99 5]
[ 7 6]]
'''
如果我們現在修改此子陣列,則不會觸及原始陣列:
x2_sub_copy[0, 0] = 42
print(x2_sub_copy)
'''
[[42 5]
[ 7 6]]
'''
print(x2)
'''
[[99 5 2 4]
[ 7 6 8 8]
[ 1 6 7 7]]
'''
陣列的形狀調整
另一種有用的操作型別是陣列的形狀調整。最靈活的方法是使用reshape
方法。例如,如果要將數字 1 到 9 放在3x3
網格中,則可以執行以下操作:
grid = np.arange(1, 10).reshape((3, 3))
print(grid)
'''
[[1 2 3]
[4 5 6]
[7 8 9]]
'''
請注意,為此,初始陣列的大小必須匹配形狀調整的陣列的大小。在可能的情況下,reshape
方法將使用初始陣列的非副本檢視,但對於非連續的記憶體緩衝區,情況並非總是如此。
另一種常見的形狀調整是將一維陣列轉換為二維行或列矩陣。這可以使用reshape
方法完成,或者通過在切片操作中使用newaxis
關鍵字更容易地完成:
x = np.array([1, 2, 3])
# 通過 reshape 來建立行向量
x.reshape((1, 3))
# array([[1, 2, 3]])
# 通過 newaxis 來建立行向量
x[np.newaxis, :]
# array([[1, 2, 3]])
# 通過 reshape 來建立列向量
x.reshape((3, 1))
'''
array([[1],
[2],
[3]])
'''
# 通過 newaxis 來建立列向量
x[:, np.newaxis]
'''
array([[1],
[2],
[3]])
'''
我們將在本書的其餘部分經常看到這種型別的轉換。
陣列的連線和分割
所有上述例程都適用於單個數組。也可以將多個數組合併為一個,並與之相反,將單個數組拆分為多個數組。我們將在這裡看看這些操作。
陣列的連線
在 NumPy 中連線兩個陣列,主要是使用例程np.concatenate
,np.vstack
和np.hstack
完成的。
np.concatenate
將陣列元組或列表作為它的第一個引數,我們可以在這裡看到:
x = np.array([1, 2, 3])
y = np.array([3, 2, 1])
np.concatenate([x, y])
# array([1, 2, 3, 3, 2, 1])
你還可以同時連線兩個以上的陣列:
z = [99, 99, 99]
print(np.concatenate([x, y, z]))
# [ 1 2 3 3 2 1 99 99 99]
它也可以用於二維陣列:
grid = np.array([[1, 2, 3],
[4, 5, 6]])
# 沿第一個軸連線
np.concatenate([grid, grid])
'''
array([[1, 2, 3],
[4, 5, 6],
[1, 2, 3],
[4, 5, 6]])
'''
# 沿第二個軸連線(下標從零開始)
np.concatenate([grid, grid], axis=1)
'''
array([[1, 2, 3, 1, 2, 3],
[4, 5, 6, 4, 5, 6]])
'''
對於處理混合維度的陣列,使用np.vstack
(垂直堆疊)和np.hstack
(水平堆疊)函式更清楚:
x = np.array([1, 2, 3])
grid = np.array([[9, 8, 7],
[6, 5, 4]])
# 垂直堆疊陣列
np.vstack([x, grid])
'''
array([[1, 2, 3],
[9, 8, 7],
[6, 5, 4]])
'''
# 水平堆疊陣列
y = np.array([[99],
[99]])
np.hstack([grid, y])
'''
array([[ 9, 8, 7, 99],
[ 6, 5, 4, 99]])
'''
類似地,np.dstack
將沿第三個軸堆疊陣列。
陣列的分割
連線的反面是分割,它由函式np.split
,np.hsplit
和np.vsplit
實現。 對於其中的每一個,我們可以傳遞索引列表來提供分割點:
x = [1, 2, 3, 99, 99, 3, 2, 1]
x1, x2, x3 = np.split(x, [3, 5])
print(x1, x2, x3)
# [1 2 3] [99 99] [3 2 1]
請注意,N
個分割點會導致N+1
個子陣列。相關函式np.hsplit
和np.vsplit
是相似的:
grid = np.arange(16).reshape((4, 4))
grid
'''
array([[ 0, 1, 2, 3],
[ 4, 5, 6, 7],
[ 8, 9, 10, 11],
[12, 13, 14, 15]])
'''
upper, lower = np.vsplit(grid, [2])
print(upper)
print(lower)
'''
[[0 1 2 3]
[4 5 6 7]]
[[ 8 9 10 11]
[12 13 14 15]]
'''
left, right = np.hsplit(grid, [2])
print(left)
print(right)
'''
[[ 0 1]
[ 4 5]
[ 8 9]
[12 13]]
[[ 2 3]
[ 6 7]
[10 11]
[14 15]]
'''
類似地,np.dsplit
將沿第三個軸分割陣列。