Python中的数据操作基本就是NumPy数组操作的同义词:一些新的工具像 Pandas 都是依赖于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个 $i^{th}$ 元素值(从0开始)可以使用中括号内的索引值获得:
x1
array([5, 0, 3, 3, 7, 9])
x1[0]
np.int64(5)
x1[4]
np.int64(7)
需要从末尾进行索引取值,你可以使用负的索引值:
x1[-1]
np.int64(9)
x1[-2]
np.int64(7)
在多维数组中获取元素值,可以在中括号中使用一个索引值的元组:
译者注:多维数组的索引方式与列表的列表索引方式是不同的。列表的列表在Python中需要使用多个中括号进行索引,如x[i][j]
的方式。
x2
array([[3, 5, 2, 4], [7, 6, 8, 8], [1, 6, 7, 7]])
x2[0, 0]
np.int64(3)
x2[2, 0]
np.int64(1)
x2[2, -1]
np.int64(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])
正如我们可以使用中括号获取单个元素值,我们也可以使用中括号的切片语法获取子数组,切片的语法遵从标准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] # 中间4~6序号的元素
array([4, 5, 6])
x[::2] # 每隔一个取元素
array([0, 2, 4, 6, 8])
x[1::2] # 每隔一个取元素,开始序号为1
array([1, 3, 5, 7, 9])
当step为负值时,将会在数组里反向的取元素,这是将数组反向排序最简单的方法:
译者注,从其他编程语言转Python的初学者,很容易问一个问题,我想反序一个字符串,怎么找不到函数啊,內建的没有,str的方法也没有。答案是,因为根本不需要,例如:
s = 'hello world'
print(s[::-1])
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] # 行的维度取前两个,列的维度取前三个,形状变为(2, 3)
array([[12, 5, 2], [ 7, 6, 8]])
x2[:3, ::2] # 行的维度取前三个(全部),列的维度每个一个取一列,形状变为(3, 2)
array([[12, 2], [ 7, 8], [ 1, 7]])
最后,子数组的各维度还可以反序:
x2[::-1, ::-1] # 行和列都反序,形状保持(3, 4)
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]
print(x2)
[[12 5 2 4] [ 7 6 8 8] [ 1 6 7 7]]
让我们从中取一个$2 \times 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]]
这个默认行为是很有用的:这意味着当我们在处理大数据集时,我们可以获取和处理其中的部分子数据集而不需要在内存中复制一份数据的副本。
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]]
grid = np.arange(1, 10).reshape((3, 3))
print(grid)
[[1 2 3] [4 5 6] [7 8 9]]
注意,改变形状要能成功,原始数组和新的形状的数组的总长度size
必须一样。当可能的情况下,reshape
会尽量使用原始数组的视图,但是如果原始数组的数据存储在不连续的内存区,就会进行复制。
另外一个常用的改变形状的操作就是将一个一维数组变成二维数组中的一行或者一列。这也可以使用reshape
方法实现,或者更简单的方式是使用切片语法中的newaxis
属性增加一个维度:
x = np.array([1, 2, 3])
# 使用reshape变为 (1, 3)
x.reshape((1, 3))
array([[1, 2, 3]])
# 使用newaxis,增加行维度,形状也是 (1, 3)
x[np.newaxis, :]
array([[1, 2, 3]])
# 使用reshape变为 (3, 1)
x.reshape((3, 1))
array([[1], [2], [3]])
# 使用newaxis增加列维度,形状也是 (3, 1)
x[:, np.newaxis]
array([[1], [2], [3]])
我们会在本书后续的内容经常看到这样的变换。
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]])
# 沿着第一个维度进行连接,即按照行连接,axis=0
np.concatenate([grid, grid])
array([[1, 2, 3], [4, 5, 6], [1, 2, 3], [4, 5, 6]])
# 沿着第二个维度进行连接,即按照列连接,axis=1
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
会沿着第三个维度(深度)进行堆叠。
x = [1, 2, 3, 99, 99, 3, 2, 1]
x1, x2, x3 = np.split(x, [3, 5]) # 在序号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]) # 沿垂直方向切分,切分点行序号为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]) # 沿水平方向切分数组,切分点列序号为2
print(left)
print(right)
[[ 0 1] [ 4 5] [ 8 9] [12 13]] [[ 2 3] [ 6 7] [10 11] [14 15]]
同样np.dsplit
会沿着第三个维度切分数组。