## 9.4 NumPy 数组的基础

> 原文：[The Basics of NumPy Arrays](https://nbviewer.jupyter.org/github/donnemartin/data-science-ipython-notebooks/blob/master/numpy/02.02-The-Basics-Of-NumPy-Arrays.ipynb)
> 
> 译者：[飞龙](https://github.com/wizardforcel)
> 
> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
> 
> 本节是[《Python 数据科学手册》](https://github.com/jakevdp/PythonDataScienceHandbook)（Python Data Science Handbook）的摘录。

Python 中的数据操作几乎与 NumPy 数组操作同义：即使是像 Pandas 这样的新工具也是围绕 NumPy 数组构建的。本节将介绍几个示例，使用 NumPy 数组操作来访问数据和子数组，以及拆分，重塑和连接数组。

虽然这里显示的操作类型可能看起来有点枯燥和怪异，但它们构成了本书中使用的许多其他示例的积木。尽快了解它们！

我们将在这里介绍几类基本数组操作：

- *数组的属性*：确定数组的大小，形状，内存消耗和数据类型
- *数组的索引*：获取和设置各个数组元素的值
- *数组切片*：在较大的数组中获取和设置较小的子数组
- *数组的重塑*：更改给定数组的形状
- *数组的连接和分割*：将多个数组合并为一个数组，并将一个数组拆分为多个数组

### NumPy 数组属性

首先让我们讨论一些有用的数组属性。

我们首先定义三个随机数组，一维，二维和三维数组。我们将使用 NumPy 的随机数生成器，并使用设定值*设置种子*，来确保每次运行此代码时，生成相同的随机数组：

```py
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``（数组的总大小）：

```py
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 中的数据类型”中讨论过）：


```py
print("dtype:", x3.dtype)

# dtype: int64
```

其他属性包括``itemsize``，它列出每个数组元素的大小（以字节为单位）和``nbytes``，它列出了数组的总大小（以字节为单位）：

```py
print("itemsize:", x3.itemsize, "bytes")
print("nbytes:", x3.nbytes, "bytes")

'''
itemsize: 8 bytes
nbytes: 480 bytes
'''
```

一般来说，我们希望``nbytes``等于``itemsize``乘以``size``。

### 数组索引：访问单个元素

如果你熟悉 Python 的标准列表索引，NumPy 中的索引将会非常眼熟。

在一维数组中，可以通过在方括号中指定所需的索引（从零开始计算），来访问第`i`值，就像使用 Python 列表一样：

```py
x1

# array([5, 0, 3, 3, 7, 9])

x1[0]

# 5

x1[4]

# 7
```

要从数组的末尾开始索引，可以使用负索引：

```py
x1[-1]

# 9


x1[-2]

# 7
```

在多维数组中，可以使用以逗号分隔的索引元组来访问项目：

```py
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
```

也可以使用以上任何索引表示法修改值：

```py
x2[0, 0] = 12
x2

'''
array([[12,  5,  2,  4],
       [ 7,  6,  8,  8],
       [ 1,  6,  7,  7]])
'''
```

请记住，与 Python 列表不同，NumPy 数组具有固定类型。

这意味着，例如，如果你尝试将浮点值插入整数数组，则该值将被静默截断。 不要意识不到这种行为！

```py
x1[0] = 3.14159  # 会截断！
x1

# array([3, 0, 3, 3, 7, 9])
```

### 数组切片：访问子数组

就像我们可以使用方括号来访问单个数组元素一样，我们也可以使用它们以及由冒号（``:``）标记的切片表示法，来访问子数组。

NumPy 切片语法遵循标准 Python 列表的语法；要访问数组``x``的切片，请使用：

```py
x[start:stop:step]
```

如果其中任何一个未指定，它们默认为``start = 0``，``stop = 维度大小``，``step = 1``。

我们看一下如何在一维和多维中访问子数组。

#### 一维子数组


```py
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``的默认值。这成为反转数组的便捷方法：

```py
x[::-1]  # 所有元素反过来

# array([9, 8, 7, 6, 5, 4, 3, 2, 1, 0])

x[5::-2]  # 反向的每个其它元素，起始于下标 5

# array([5, 3, 1])
```

#### 多维子数组

多维切片以相同的方式工作，多个切片用逗号分隔。例如：

```py
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]])
'''
```

最后，子数组的维度甚至可以一起反转：

```py
x2[::-1, ::-1]

'''
array([[ 7,  7,  6,  1],
       [ 8,  8,  6,  7],
       [ 4,  2,  5, 12]])
'''
```

#### 访问数组的行和列

一个常用的例程是访问数组的单个行或列。

这可以通过组合索引和切片来完成，使用由单个冒号（``:``）标记的空切片：

```py
print(x2[:, 0])  # x2 的第一列 

# [12  7  1]

print(x2[0, :])  # x2 的第一行

# [12  5  2  4]
```

在访问行的情况下，可以省略空切片来获得更紧凑的语法：

```py
print(x2[0])  # 等价于 x2[0, :]

# [12  5  2  4]
```

#### 作为无副本视图的子数组

数组切片的一个重要且非常有用的事情，是它们返回*视图*而不是数组数据的*副本*。这是 NumPy 数组切片与 Python 列表切片的不同之处：在列表中，切片是副本。

考虑我们之前的二维数组：

```py
print(x2)

'''
[[12  5  2  4]
 [ 7  6  8  8]
 [ 1  6  7  7]]
'''
```

让我们从中提取`2x2`子数组：

```py
x2_sub = x2[:2, :2]
print(x2_sub)

'''
[[12  5]
 [ 7  6]]
'''
```

现在，如果我们修改这个子数组，我们会看到原始数组已经改变了！注意：

```py
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()``方法可以很容易地做到：

```py
x2_sub_copy = x2[:2, :2].copy()
print(x2_sub_copy)

'''
[[99  5]
 [ 7  6]]
'''
```

如果我们现在修改此子数组，则不会触及原始数组：

```py
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`网格中，则可以执行以下操作：

```py
grid = np.arange(1, 10).reshape((3, 3))
print(grid)

'''
[[1 2 3]
 [4 5 6]
 [7 8 9]]
'''
```

请注意，为此，初始数组的大小必须匹配形状调整的数组的大小。在可能的情况下，``reshape``方法将使用初始数组的非副本视图，但对于非连续的内存缓冲区，情况并非总是如此。

另一种常见的形状调整是将一维数组转换为二维行或列矩阵。这可以使用``reshape``方法完成，或者通过在切片操作中使用``newaxis``关键字更容易地完成：

```py
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``将数组元组或列表作为它的第一个参数，我们可以在这里看到：


```py
x = np.array([1, 2, 3])
y = np.array([3, 2, 1])
np.concatenate([x, y])

# array([1, 2, 3, 3, 2, 1])
```

你还可以同时连接两个以上的数组：

```py
z = [99, 99, 99]
print(np.concatenate([x, y, z]))

# [ 1  2  3  3  2  1 99 99 99]
```

它也可以用于二维数组：

```py
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``（水平堆叠）函数更清楚：

```py
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``实现。 对于其中的每一个，我们可以传递索引列表来提供分割点：

```py
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``是相似的：

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