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
将沿第三个轴分割数组。