Python 中的数据操作几乎等同于 NumPy 数组操作:甚至像 Pandas(第 3 章)这样的更新工具也是围绕 NumPy 数组构建的。本节将展示几个使用 NumPy 数组操作访问数据和子数组以及拆分、重塑和连接数组的示例。虽然这里展示的操作类型可能看起来有点枯燥和迂腐,但它们构成了本书中使用的许多其他示例的构建块。好好认识他们!
我们将在这里介绍几类基本数组操作:
- 数组的属性:确定数组的大小、形状、内存消耗和数据类型
- 数组索引:获取和设置单个数组元素的值
- 数组切片:在较大的数组中获取和设置较小的子数组
- 数组重塑:改变给定数组的形状
- 数组的合并和拆分:将多个数组合并为一个数组,将一个数组拆分为多个数组
Numpy数组属性
首先让我们讨论一些有用的数组属性。我们将从定义三个随机数组开始,分别是一维、二维和三维数组。我们将使用 NumPy 的随机数生成器,并使用一个设定值作为种子,以确保每次运行此代码时生成相同的随机数组:
import numpy as np
np.random.seed(0) # 设置重现的种子
x1 = np.random.randint(10, size=6) # 1维数组
x2 = np.random.randint(10, size=(3, 4)) # 2维数组
x3 = np.random.randint(10, size=(3, 4, 5)) # 3维数组
每个数组都有属性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,数组的数据类型(这在上一节有说过):
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 中的索引非常熟悉。在一维数组中,可以通过在方括号中指定所需的索引来访问 值(从零开始计数),就像 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])
数组切片:访问子数组
正如我们可以使用方括号访问单个数组元素一样,我们也可以使用它们访问带有切片符号的子数组,以冒号 (:) 字符标记。
x[start:stop:step]
如果其中任何一个未指定,它们默认为值 start=0,stop=size of dimension,step=1。我们将看看访问一维和多维的子数组。
一维子数组
x = np.arange(10)
x
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
x[:5] # 前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])
一个可能令人困惑的情况是步长值为负。在这种情况下,启动和停止的默认值被交换。这成为反转数组的便捷方式:
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的第1列
[12 7 1]
print(x2[0, :]) # x2的第1行
[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]]
让我们提取一个2×2的子数组:
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]]
重塑数组
另一种有用的操作类型是数组的整形。最灵活的方法是使用重塑方法。例如,如果要将数字 1 到 9 放在 3×3 网格,您可以执行以下操作:
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])
也可以一次压缩多于2个的数组:
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]])
# 沿着第二轴压缩 (zero-indexed)
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会沿第三轴分裂数组。