结构化数据:NumPy 的结构化数组
虽然我们的数据通常可以用同质数组很好地表示,但有时情况并非如此。本文将演示 NumPy 的结构化数组和记录数组的用法,它们为复合的、异构的数据提供了高效的存储方式。虽然这里展示的模式适用于简单操作,但此类场景通常更适合使用 Pandas 的 DataFrame
。
import numpy as np
假设我们有几类关于多个人的数据(比如姓名、年龄和体重),并且我们希望将这些值存储起来以便在 Python 程序中使用。
我们可以将它们分别存储在三个独立的数组中:
name = ['Alice', 'Bob', 'Cathy', 'Doug']
age = [25, 45, 37, 19]
weight = [55.0, 85.5, 68.0, 61.5]
但是这样做有些笨拙,实际使用起来并不方便,因为这里没有任何内容能表明这三个数组是相关联的;NumPy 的结构化数组允许我们通过使用单一结构来更自然地存储所有这些数据。
回想一下,之前我们使用如下表达式创建了一个简单的数组:
x = np.zeros(4, dtype=int)
我们同样可以使用复合数据类型规范来创建结构化数组:
# 使用复合数据类型创建结构化数组
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 位)浮点数”。
我们将在下一节讨论这些类型代码的其他选项。
现在我们已经创建了一个空的容器数组,可以用我们的值列表来填充这个数组了:
data['name'] = name
data['age'] = age
data['weight'] = weight
print(data)
[('Alice', 25, 55. ) ('Bob', 45, 85.5) ('Cathy', 37, 68. )
('Doug', 19, 61.5)]
如我们所愿,数据现在已经方便地排列在一个结构化数组中。
结构化数组的一个便利之处在于,我们现在既可以通过索引,也可以通过字段名来引用数值:
# 获取所有姓名
data['name']
array(['Alice', 'Bob', 'Cathy', 'Doug'], dtype='<U10')
# 获取第一行数据
data[0]
np.void(('Alice', 25, 55.0), dtype=[('name', '<U10'), ('age', '<i4'), ('weight', '<f8')])
# 获取最后一行的姓名
data[-1]['name']
np.str_('Doug')
通过布尔掩码,我们甚至可以进行更复杂的操作,比如根据年龄进行筛选:
# 获取小于30岁人员的姓名
data[data['age'] < 30]['name']
array(['Alice', 'Doug'], dtype='<U10')
如果你想进行比这些更复杂的操作,建议你考虑使用 Pandas 包。
正如你将看到的,Pandas 提供了一个 DataFrame
对象,这是一种基于 NumPy 数组构建的数据结构,提供了多种有用的数据操作功能,类似于你在这里看到的内容,而且功能远不止这些。
探索结构化数组的创建
结构化数组的数据类型可以通过多种方式指定。
前面我们已经看到了字典的方法:
np.dtype({'names':('name', 'age', 'weight'),
'formats':('U10', 'i4', 'f8')})
dtype([('name', '<U10'), ('age', '<i4'), ('weight', '<f8')])
为清晰起见,数值类型也可以使用 Python 类型或 NumPy 的 dtype
来指定:
np.dtype({'names':('name', 'age', 'weight'),
'formats':((np.str_, 10), int, np.float32)})
dtype([('name', '<U10'), ('age', '<i8'), ('weight', '<f4')])
复合类型也可以用元组列表的形式指定:
np.dtype([('name', 'S10'), ('age', 'i4'), ('weight', 'f8')])
dtype([('name', 'S10'), ('age', '<i4'), ('weight', '<f8')])
如果你不关心类型的名称,也可以仅用逗号分隔的字符串来指定类型:
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
字段,该字段是一个 3 × 3 3\times 3 3×3 的浮点型矩阵:
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
和一个 3 × 3 3\times 3 3×3 的矩阵。
为什么要使用这种方式,而不是简单的多维数组,或者 Python 字典呢?
其中一个原因是,这种 NumPy 的 dtype
可以直接映射到 C 语言的结构体定义,因此包含数组内容的缓冲区可以被适当编写的 C 程序直接访问。
如果你需要为操作结构化数据的遗留 C 或 Fortran 库编写 Python 接口,结构化数组可以提供强大的接口能力。
记录数组:结构化数组的变体
NumPy 还提供了记录数组(np.recarray
类的实例),它们与前面介绍的结构化数组几乎完全相同,但有一个额外的特性:字段可以作为属性访问,而不仅仅是作为字典键访问。
回想一下,我们之前通过如下方式访问示例数据集中的年龄字段:
data['age']
array([25, 45, 37, 19], dtype=int32)
如果我们将数据视为记录数组(record array),就可以用更简洁的方式访问字段:
data_rec = data.view(np.recarray)
data_rec.age
array([25, 45, 37, 19], dtype=int32)
缺点是,对于记录数组,即使使用相同的语法,访问字段时也会有一些额外的开销:
%timeit data['age']
%timeit data_rec['age']
%timeit data_rec.age
34 ns ± 0.458 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)
481 ns ± 12.6 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
975 ns ± 12.8 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each)
这种更方便的记法是否值得(略微的)额外开销,将取决于你的具体应用场景。
走向 Pandas
本文关于结构化数组和记录数组的内容之所以安排在本部分的结尾,是因为它很好地引出了我们接下来要介绍的包:Pandas。
结构化数组在某些场景下非常有用,比如你需要用 NumPy 数组映射到 C、Fortran 或其他语言中的二进制数据格式时。
但在日常的结构化数据处理中,Pandas 包无疑是更好的选择;在接下来的章节中,我们将深入探讨它。