## 9.11 结构化数据：NumPy 的结构化数组

> 原文：[Structured Data: NumPy's Structured Arrays](https://nbviewer.jupyter.org/github/donnemartin/data-science-ipython-notebooks/blob/master/numpy/02.09-Structured-Data-NumPy.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）的摘录。

虽然我们的数据通常可以通过同构数组来很好地表示，但有时并非如此。 本节演示了 NumPy 结构化数组和记录数组的用法，它们为复合异构数据提供了有效的存储。 虽然这里展示的模式对于简单操作很有用，但像这样的场景通常适合使用 Pandas `Dataframe`，我们将在第三章中探索。

```py
import numpy as np
```

想象一下，我们有很多人的多个数据类别（比如姓名，年龄和体重），我们希望存储这些值以便在 Python 程序中使用。可以将它们存储在三个独立的数组中：

```py
name = ['Alice', 'Bob', 'Cathy', 'Doug']
age = [25, 45, 37, 19]
weight = [55.0, 85.5, 68.0, 61.5]
```

但这有点笨拙。 这里没有任何东西告诉我们三个数组是相关的；如果我们可以使用单一结构来存储所有这些数据，那将更自然。NumPy 可以使用结构化数组处理这个问题，结构化数组是具有复合数据类型的数组。

回想一下，之前我们使用这样的表达式创建了一个简单的数组：

```py
x = np.zeros(4, dtype=int)
```

我们可以使用复合数据类型规范，以相似方式创建结构化数组：

```py
# 使用结构化数组的复合数据类型
data = np.zeros(4, dtype={'names':('name', 'age', 'weight'),
                          'formats':('U10', 'i4', 'f8')})
print(data.dtype)

# [('name', '<U10'), ('age', '<i4'), ('weight', '<f8')]
```

这里`'U10'`表示“最大长度为 10 的 Unicode 字符串”，`'i4'`表示 4 字节（即 32 位）整数，`'f8'`表示 8 字节（即 64 位）浮点数。我们将在下一节中讨论这些类型代码的其他选项。

现在我们已经创建了一个空的容器数组，我们可以使用我们的值列表填充数组：

```py
data['name'] = name
data['age'] = age
data['weight'] = weight
print(data)

'''
[('Alice', 25, 55.0) ('Bob', 45, 85.5) ('Cathy', 37, 68.0)
 ('Doug', 19, 61.5)]
'''
```

正如我们所希望的那样，数据现在被安排在一个方便的内存块中。结构化数组的便利之处在于，你现在可以通过索引或名称来引用值：

```py
# 获取所有名称
data['name']

'''
array(['Alice', 'Bob', 'Cathy', 'Doug'], 
      dtype='<U10')
'''

# 获取数据的第一行
data[0]

# ('Alice', 25, 55.0)

# 获取最后一行的名称
data[-1]['name']

# 'Doug'
```

使用布尔掩码，你甚至可以执行一些更复杂的操作，例如过滤年龄：

```py
# 获取年龄小于 30 的名称
data[data['age'] < 30]['name']

'''
array(['Alice', 'Doug'], 
      dtype='<U10')
'''
```

请注意，如果你想进行任何比这些更复杂的操作，你应该考虑下一章中介绍的 Pandas 包。正如我们所看到的，Pandas 提供了``Dataframe``对象，它是一个构建在 NumPy 数组上的结构，它提供了各种有用的数据操作功能，类似于我们在这里展示的东西，以及更多。

### 创建结构化数组

可以通过多种方式规定结构化数组数据类型。之前，我们见过了字典方法：


```py
np.dtype({'names':('name', 'age', 'weight'),
          'formats':('U10', 'i4', 'f8')})

# dtype([('name', '<U10'), ('age', '<i4'), ('weight', '<f8')])
```

为清楚起见，可以使用 Python 类型或 NumPy `dtype`来指定数字类型：

```py
np.dtype({'names':('name', 'age', 'weight'),
          'formats':((np.str_, 10), int, np.float32)})
          
# dtype([('name', '<U10'), ('age', '<i8'), ('weight', '<f4')])
```

复合类型也可以指定为元组列表：

```py
np.dtype([('name', 'S10'), ('age', 'i4'), ('weight', 'f8')])

# dtype([('name', 'S10'), ('age', '<i4'), ('weight', '<f8')])
```

如果类型的名称对你无关紧要，则可以在逗号分隔的字符串中单独指定类型：

```py
np.dtype('S10,i4,f8')

# dtype([('f0', 'S10'), ('f1', '<i4'), ('f2', '<f8')])
```

缩短的字符串格式代码可能看起来令人困惑，但它们建立在简单的原则之上。第一个（可选）字符是``<``或``>``，分别表示“小端”或“大端”，并规定了有效位的顺序约定。下一个字符指定数据类型：字符，字节，整数，浮点等（参见下表）。最后一个或多个字符表示对象的大小（以字节为单位）。

| 字符        | 描述           | 示例                             |
| ---------        | -----------           | -------                             | 
| ``'b'``          | 字节                  | ``np.dtype('b')``                   |
| ``'i'``          | 符号整数        | ``np.dtype('i4') == np.int32``      |
| ``'u'``          | 无符号整数      | ``np.dtype('u1') == np.uint8``      |
| ``'f'``          | 浮点        | ``np.dtype('f8') == np.int64``      |
| ``'c'``          | 复数浮点 | ``np.dtype('c16') == np.complex128``|
| ``'S'``, ``'a'`` | 字符串                | ``np.dtype('S5')``                  |
| ``'U'``          | Unicode 字符串        | ``np.dtype('U') == np.str_``        |
| ``'V'``          | 原始数据（void）       | ``np.dtype('V') == np.void``        |

### 更高级的复合类型

可以定义更高级的复合类型。例如，你可以创建一个类型，其中每个元素包含一个数组或矩阵。在这里，我们将创建一个带有``mat``成分的数据类型，该成分由`3x3`浮点矩阵组成：

```py
tp = np.dtype([('id', 'i8'), ('mat', 'f8', (3, 3))])
X = np.zeros(1, dtype=tp)
print(X[0])
print(X['mat'][0])

'''
(0, [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]])
[[ 0.  0.  0.]
 [ 0.  0.  0.]
 [ 0.  0.  0.]]
'''
```

现在``X``数组中的每个元素都包含一个``id``和一个`3x3`矩阵。为什么要使用它而不是简单的多维数组，或者 Python 字典呢？原因是这个 NumPy ``dtype``直接映射到 C 结构定义，因此包含数组内容的缓冲区，可以在适当编写的 C 程序中直接访问。

如果你发现自己为处理结构化数据的遗留 C 或 Fortran 库编写 Python 接口，你可能会发现结构化数组非常有用！

### 记录数组：略有不同的结构化数组

NumPy 还提供了``np.recarray``类，它与刚刚描述的结构化数组几乎相同，但有一个附加功能：字段可以作为属性而不是字典的键来访问。

回想一下，我们以前写过：

```py
data['age']

# array([25, 45, 37, 19], dtype=int32)
```

如果我们将数据视为记录数组，我们可以通过更少的敲键盘来访问它：

```py
data_rec = data.view(np.recarray)
data_rec.age

# array([25, 45, 37, 19], dtype=int32)
```

缺点是对于记录数组，即使使用相同的语法，访问字段会有一些额外的开销。 我们在这里可以看到：

```py
%timeit data['age']
%timeit data_rec['age']
%timeit data_rec.age

'''
1000000 loops, best of 3: 241 ns per loop
100000 loops, best of 3: 4.61 µs per loop
100000 loops, best of 3: 7.27 µs per loop
'''
```

更方便的记号是否值得额外开销，取决于你自己的应用。

### 转向 Pandas

关于结构化和记录数组的这一部分，有意放在本章的最后部分，因为它很好地介绍了我们将要介绍的下一个包：Pandas。

在某些情况下，最好了解这里讨论的结构化数组，特别是在你使用 NumPy 数组来映射到 C，Fortran 或其他语言的二进制数据格式的情况下。

对于结构化数据的日常使用，Pandas 包是一个更好的选择，我们将在下一章中深入讨论它。
