访问数组多维度的内容

在本节中,我们将会:

  • 定义数组的“维度”(dimensionality)。

  • 讨论N维数组(ND-array)的作用。

  • 介绍访问多维数组内容的索引和切片体系。

我们将会遇到不同维度的数组:

# 0维数组
np.array(8)

# 1维数组,形状为 (3,)
np.array([2.3, 0.1, -9.1])

# 2维数组,形状为 (3, 2)
np.array([[93,  95],
          [84, 100],
          [99,  87]])

# 3维数组,形状为 (2, 2, 2)
np.array([[[0, 1],
           [2, 3]],

          [[4, 5],
           [6, 7]]])

和Python的序列类似,我们使用0开始的索引和切片来获取数组中的内容。但是,我们必须为数组的每个维度提供索引/切片:

>>> import numpy as np

# 3维数组
>>> x = np.array([[[0, 1],
...                [2, 3]],
...
...               [[4, 5],
...                [6, 7]]])

# 获取:第0页,全部两行,反向排列的列
>>> x[0, :, ::-1]
array([[1, 0],
       [3, 2]])

一维数组

让我们首先创建一额简单的N维数组,其成员为三个浮点数。

>>> simple_array = np.array([2.3, 0.1, -9.1])

这个数组支持和Python序列(列表,元组,和字符串)一样的索引体系:

+------+------+------+
|  2.3 |  0.1 | -9.1 |
+------+------+------+
    0      1      2
   -3     -2     -1

第一行数字提供了索引0-2在数组中对应的位置;第二行则提供了对应的负索引。从 \(i\)\(j\) 的切片将返回一个在标记索引为 \(i\)\(j\) 的成员之间所有的值(包括 \(i\) 但不包括 \(j\)):

>>> simple_array[0]
2.3

>>> simple_array[-2]
0.1

>>> simple_array[1:3]
array([ 0.1, -9.1])

>>> simple_array[3]
IndexError: index 3 is out of bounds for axis 0 with size 3

根据以上的索引体系,你只需要一个整数就能无歧义地辨别数组中的一个成员。相应的,你只需要一个切片就能无歧义地辨别数组中的一个子序列。因此,我们称其为一维数组(1-dimensional array)。笼统来讲,数组的维度(dimensionality)描述了无歧义辨别数组中单个成员所需要的索引数量。

定义

数组的维度(dimensionality)指无歧义地辨别其单个成员所需要的索引数量。

这个维度的定义在NumPy之外也通用;你需要三个数字来无歧义地描述现实空间中的一个点,所以我们认为空间有着三个维度。

二维数组

在我们开始讨论高维数组之前,让我们先讨论一个很简单的需要在多维度获取数据的数据集。某成绩簿有着以下表格:

考试1 (%)

考试2 (%)

Ashley

\(93\)

\(95\)

Brad

\(84\)

\(100\)

Cassie

\(99\)

\(87\)

这个数据集总共有6个成绩值。将其存为一维数组明显是不智的:

# 使用1维数组来储存成绩
>>> grades = np.array([93, 95, 84, 100, 99, 87])

虽然我们没有丢失任何数据,但是使用单个索引来访问其中的数据很不方便;我们希望能在获取成绩时提供学生和需要的考试——所以说使用两个维度来储存这些数据是再自然不过的。让我们创建一个存储这些成绩的二维数组:

# 使用2维数组来储存成绩
>>> grades = np.array([[93,  95],
...                    [84, 100],
...                    [99,  87]])

NumPy能够处理向 np.array 输入的成员为列表的列表,并根据其重复结构将其存为一个二维数组,第一维为“学生”维度,第二维度为“考试”维度。

轴心 vs 维度:

虽然NumPy正式承认维度这一概念且对其进行了和本节一样的定义,但是它的说明文档将数组的单个维度称为一个(axis,复数为axes)。因此你有时会见到说明文档用“axes”代替“dimensions”;但是,它们指着同样的东西。

NumPy将二维数组的行轴(学生)称为“轴0”,列轴(exams)称为“轴1”。你必须提供“两个”索引(每个轴心(维度)各一个)来无歧义地辨别这个二维数组中的一个成员;第一个数字提供了在轴0上的索引,第二个数字提供了在轴1上的索引。不管是哪个轴心,我们之前复习的从0开始的索引体系都通用:

            -- axis-1 ->
              -2  -1
               0   1
  |          +---+---+
  |    -3, 0 |93 | 95|
  |          +---+---+
axis-0 -2, 1 |84 |100|
  |          +---+---+
  |    -1, 2 |99 | 87|
  V          +---+---+

因为 grades 在轴0上有三行数据,在轴1上由两行数据,它的“形状”(shape)为 (3, 2)

>>> grades.shape
(3, 2)

整数索引

因此,如果我们想要获取Brad(轴0上的项目1)的考试1(轴1上的项目0)的成绩,我们只需要提供:

# 提供两个数字来访问2维数组的成员
>>> grades[1, 0]  # Brad考试1的成绩
84

# 负索引和列表/元组/字符串一样可以使用
>>> grades[-2, 0]  # Brad考试1的成绩
84

切片索引

我们也可以使用切片(slice)来获取数据的子序列。假设我们想要获取所有成绩在考试2的成绩,我们可以在轴0上从0到3切片(请参考之前小节的索引图)来包含所有学生,并在轴1上提供索引1来选中考试2:

>>> grades[0:3, 1]  # 所有学生的考试2成绩
array([ 95, 100,  87])

和Python序列一样,你可以提供一个“空”切片来默认包含某轴的所有项目:在这个序列中 grades[:, 1] 等值于 grades[0:3, 1]。更笼统的来讲,跳过不填切片的 ‘start’ 或 ‘stop’ 值会分别默认使用最小或最大的合法索引:

>>> grades[1:, 1]  # 等值于 `grades[1:3, 1]`
array([ 100,  87])

>>> grades[:, :1]  # 等值于 `grades[0:3, 0:1]`
array([[93],
       [84],
       [99]])

grades[:, :1] 的输出可能看起来有点奇怪。因为轴1切片仅仅包含了一列的数字,最后得到的数组的形状为 (3, 1)。所以,在轴1上0是唯一的合法(非负)索引,因为在这个得到的数组中只有一行可以选中。

你也可以为切片提供一个“step”(步距)值。grades[::-1, :] 会返回学生轴颠倒的成绩数组。

负索引

如上所述,负索引也合法,且很有用。如果我们想要访问所有学生最新的考试成绩,我们可以编写:

# 使用负索引和切片
>>> grades[:, -1]  # 所有学生的最新考试成绩(考试2)
array([ 95, 100,  87])

请注意使用负索引的价值在于它永远都会为你提供最新的考试成绩——你不需要先检查学生考过多少次。

提供低于维度数量的索引

在我们为这个数组只提供一个索引时会发生什么?你可能会惊讶于 grades[0] 并不会报错这一事,因为我们为二维数组只提供了一个索引。反而,NumPy将会返回学生0(Ashley)的所有考试成绩:

>>> grades[0]
array([ 93, 95])

这是因为NumPy会在你不提供足够多的索引时自动在结尾提供缺失的切片。grades[0] 会被当作 grades[0, :] 处理。

假设你有着一个 \(N\) 维数组并在索引时仅仅提供了 \(j\) 个索引;NumPy将会在结尾自动插入 \(N-j\) 个切片。如果 \(N=5\)\(j=3\),那么 d5_array[0, 0, 0] 等值于 d5_array[0, 0, 0, :, :]

到现在为止,我们讨论了一些在数组中获取数据的规则,但这些规则都属于NumPy说明文档规定为“基础索引”(basic indexing)的一部分。我们将在之后一节完整地讨论基础索引和“进阶索引”(advanced indexing)。但是,请注意,在这里复习的所有索引/切片方法都会创建原本数组的一个“视阈“(view)。也就是说,当你使用整数索引或切片索引数组时,没有任何数据被复制。请回忆,切片列表和元组复制其数据。

供参考

记录数组每个维度的含义可能在处理现实中的数据集时很快变得麻烦复杂。xarray则是一个提供和NumPy相似功能的,但允许用户为数组的维度提供显式标签的Python模组。也就是说,你可以为每个维度命名。比如说,你可以如下使用 xarray 来选中Brad的成绩:grades.sel(student='Brad')。在你有空的时候,这是一个值得花些时间去了解的模组。

N维数组

让我们对维度高于2的数组产生一些直观的理解。以下代码创建一个三维数组:

# 三维数组,形状为 (2, 2, 2)
>>> d3_array = np.array([[[0, 1],
...                       [2, 3]],
...
...                      [[4, 5],
...                       [6, 7]]])

你可以认为轴0代表着选中哪个2x2的“页”(sheet),然后轴1代表着在页中选中哪一行,而轴2代表着在行中选择哪个列:

描绘3维数组的结构

sheet 0:
       [0, 1]
       [2, 3]

sheet 1:
       [4, 5]
       [6, 7]
   |       -- axis-2 ->
   |    |
   |  axis-1 [0, 1]
   |    |    [2, 3]
   |    V
axis-0
   |      -- axis-2 ->
   |    |
   |  axis-1 [4, 5]
   |    |    [6, 7]
   V    V

如此,d3_array[0, 1, 0] 选中了在页0,行1,列0的成员:

# 在3维数组中获得单个成员
>>> d3_array[0, 1, 0]
2

d3_array[:, 0, 0] 选中了全部页在行0和列0的成员:

# 在3维数组中获得1维子数组
>>> d3_array[:, 0, 0]
array([0, 4])

d3_array[1],也就是 d3_array[1, :, :] 的简写,选中页1全部的行和列:

# 在3维数组中获得2维子数组
>>> d3_array[1]
array([[4, 5],
       [6, 7]])

在四维的情况下,你可以将轴0的成员理解为“一(stack)有着行列的页”。轴0选中哪一打页,轴1选中哪一页,轴2选中哪一行,轴3选中哪一列。往更高维度扩展这个类比(“好多打页的集合”)将像这个相同的冗长的方式继续。

阅读理解:多维索引

设3维数组,其形状为 (3, 3, 3):

>>> arr = np.array([[[ 0,  1,  2],
...                  [ 3,  4,  5],
...                  [ 6,  7,  8]],
...
...                 [[ 9, 10, 11],
...                  [12, 13, 14],
...                  [15, 16, 17]],
...
...                 [[18, 19, 20],
...                  [21, 22, 23],
...                  [24, 25, 26]]])

索引该数组来得到以下结果

#1
array([[ 2,  5,  8],
       [11, 14, 17],
       [20, 23, 26]])

#2
array([[ 3,  4,  5],
       [12, 13, 14]])

#3
array([2, 5])

#4
array([[11, 10,  9],
       [14, 13, 12],
       [17, 16, 15]])

零维数组

零维数组仅仅是一个数字(也就是一个标量值):

# 创建一个0维数组
>>> x = np.array(15.2)

等值于长度为1的一维数组:np.array([15.2])。根据维度的定义,我们需要个数字来索引一个0维数组,因为我们不需要为单个数字其提供任何索引。因此,你不能索引0维数组。

# 你不能索引0维数组
>>> x[0]
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-10-2f755f117ac9> in <module>()
----> 1 x[0]

IndexError: too many indices for array

你必须使用语法 arr.item() 来获取0维数组的数字值:

>>> x.item()
15.2

零维数组在实际使用时不会经常出现。但是,它们的行为证明了NumPy对其数组维度的定义是自治的,而你也应该至少接触以下并理解0维数组的细节。

经验

虽然在多个维度访问数据最终其实也就是一个细心活(因为你理论上可以通过一个一维数组来访问同样的数据),但是NumPy为用户提供的多维度访问数组内容的界面是非常有用的。它为我们提供了给数据赋予直观的抽象结构的能力。

操作数组

NumPy提供了一系列操作如何访问数组数据的方法。这些函数能让我们重塑(reshape)数组的形状,修改它的维度,并交换它不同轴的位置:

>>> x = np.array([[ 1,  2,  3,  4],
...               [ 5,  6,  7,  8],
...               [ 9, 10, 11, 12]])

# 重塑数组形状
>>> x.reshape(3, 2, 2)
array([[[ 1,  2],
        [ 3,  4]],

       [[ 5,  6],
        [ 7,  8]],

       [[ 9, 10],
        [11, 12]]])

# 转制(transpose)数组:交换数组轴的顺序。
# 这将 `x` 的行列互换
>>> x.transpose()
array([[ 1,  5,  9],
       [ 2,  6, 10],
       [ 3,  7, 11],
       [ 4,  8, 12]])

完整的数组操作函数列表可以在NumPy的官方说明文档找到。在这些函数中,reshape函数极其有用。

介绍 reshape 函数

reshape 函数允许你修改数组的维度和轴结构。它修改用来访问数组内置数据的本模组之前讨论过的索引界面。设一个形状为 (6,) 的数组,让我们将其重塑成形为 (2, 3) 的数组:

>>> import numpy as np
>>> x = np.array([0, 1, 2, 3, 4, 5])

# 将形状为 (6,) 的数组重塑为形状为 (2,3) 的数组
>>> x.reshape(2, 3)
array([[0, 1, 2],
       [3, 4, 5]])

你也可以很方便地通过在赋值时“设置”它的形状来达到重塑的目的:

# equivalent to: x = x.reshape(2, 3)
>>> x.shape = (2, 3)

当然,最初数组的大小要对应重塑后的数组的大小:

# 一个有5个数字的数组不能被重塑为形状是 (3, 2) 的数组
>>> np.array([0, 1, 2, 3, 4]).reshape(3, 2)
ValueError: total size of new array must be unchanged

多维数组也可以被重塑:

# 重塑多维数组的形状
>>> x = np.array([[ 0,  1,  2,  3],
...               [ 4,  5,  6,  7],
...               [ 8,  9, 10, 11]])

# 从 (3, 4) 重塑为 (2, 3, 2)
>>> x.reshape(2, 3, 2)
array([[[ 0,  1],
        [ 2,  3],
        [ 4,  5]],

       [[ 6,  7],
        [ 8,  9],
        [10, 11]]])

因为输入数组的大小和重塑数组的大小必须相同,你可以在reshape函数中将维度大小之中的一个设置为-1.这将告诉NumPy去为你计算该维度的大小。比如说,如果你在将形状 (36,) 的数组重塑为形状是 (3, 4, 3) 的数组,以下都可以:

# 等值的重塑方法
# np.arange(36) 返回一个形状为 (36,) 的数组([0, 1, 2, ..., 35])
np.arange(36).reshape(3, 4, 3)   # (36,) --重塑--> (3, 4, 3)
np.arange(36).reshape(3, 4, -1)  # NumPy 将 -1 代替为 36/(3*4) -> 3
np.arange(36).reshape(3, -1, 3)  # NumPy 将 -1 代替为 36/(3*3) -> 4
np.arange(36).reshape(-1, 4, 3)  # NumPy 将 -1 代替为 36/(3*4) -> 3

你只能用-1来描述一个维度:

>>> np.arange(36).reshape(3, -1, -1)  # 这可能有歧义,所以...
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-3-207d18d18af2> in <module>()
----> 1 np.arange(36).reshape(3, -1, -1)

ValueError: can only specify one unknown dimension

重塑并不会复制数组

任何直接运用reshape的情况下,NumPy都不会复制原本数组数据。实际上,原本的数组和重塑的数组都引用着相同的内在数据。重塑的数组仅仅提供了一个新的访问该数据的索引界面,所以它被是原本数组的一个“视阈”(view)(我们会在后面一节详细讨论“视阈”)。

阅读理解答案:

阅读理解:多维索引

>>> arr = np.array([[[ 0,  1,  2],
...                  [ 3,  4,  5],
...                  [ 6,  7,  8]],
...
...                 [[ 9, 10, 11],
...                  [12, 13, 14],
...                  [15, 16, 17]],
...
...                 [[18, 19, 20],
...                  [21, 22, 23],
...                  [24, 25, 26]]])
#1
>>> arr[:, :, 2]
array([[ 2,  5,  8],
       [11, 14, 17],
       [20, 23, 26]])

#2
>>> arr[0:2, 1, :]
array([[ 3,  4,  5],
       [12, 13, 14]])

#3
>>> arr[0, :2, 2]
array([2, 5])

#4
>>> arr[1, :, ::-1]
array([[11, 10,  9],
       [14, 13, 12],
       [17, 16, 15]])