结构化和记录式数组
你可能已经注意到了,到目前为止我们所讨论的ndarray都是一种同质数据容器,也就是说,在它所表示的内存块中,各元素占用的字节数相同(具体根据dtype而定)。从表面上看,它似乎不能用于表示异质或表格型的数据。结构化数组是一种特殊的ndarray,其中的各个元素可以被看做C语言中的结构体(struct,这就是“结构化”的由来)或SQL表中带有多个命名字段的行:
- In [142]: dtype = [('x', np.float64), ('y', np.int32)]
- In [143]: sarr = np.array([(1.5, 6), (np.pi, -2)], dtype=dtype)
- In [144]: sarr
- Out[144]:
- array([(1.5, 6), (3.141592653589793, -2)],
- dtype=[('x', '<f8'), ('y', '<i4')])
定义结构化dtype(请参考NumPy的在线文档)的方式有很多。最典型的办法是元组列表,各元组的格式为(field_name,field_data_type)。这样,数组的元素就成了元组式的对象,该对象中各个元素可以像字典那样进行访问:
- In [145]: sarr[0]
- Out[145]: (1.5, 6)
- In [146]: sarr[0]['y']
- Out[146]: 6
字段名保存在dtype.names属性中。在访问结构化数组的某个字段时,返回的是该数据的视图,所以不会发生数据复制:
- In [147]: sarr['x']
- Out[147]: array([ 1.5 , 3.1416])
嵌套dtype和多维字段
在定义结构化dtype时,你可以再设置一个形状(可以是一个整数,也可以是一个元组):
- In [148]: dtype = [('x', np.int64, 3), ('y', np.int32)]
- In [149]: arr = np.zeros(4, dtype=dtype)
- In [150]: arr
- Out[150]:
- array([([0, 0, 0], 0), ([0, 0, 0], 0), ([0, 0, 0], 0), ([0, 0, 0], 0)],
- dtype=[('x', '<i8', (3,)), ('y', '<i4')])
在这种情况下,各个记录的x字段所表示的是一个长度为3的数组:
- In [151]: arr[0]['x']
- Out[151]: array([0, 0, 0])
访问arr['x']即可得到一个二维数组,而不是前面那个例子中的一维数组:
- In [152]: arr['x']
- Out[152]:
- array([[0, 0, 0],
- [0, 0, 0],
- [0, 0, 0],
- [0, 0, 0]])
这就使我们能用单个数组的内存块存放复杂的嵌套结构。既然dtype可以想怎么复杂就怎么复杂,那为什么不试试嵌套dtype呢?下面是一个简单的例子:
- In [153]: dtype = [('x', [('a', 'f8'), ('b', 'f4')]), ('y', np.int32)]
- In [154]: data = np.array([((1, 2), 5), ((3, 4), 6)], dtype=dtype)
- In [155]: data['x']
- Out[155]:
- array([(1.0, 2.0), (3.0, 4.0)],
- dtype=[('a', '<f8'), ('b', '<f4')])
- In [156]: data['y']
- Out[156]: array([5, 6], dtype=int32)
- In [157]: data['x']['a']
- Out[157]: array([ 1., 3.])
不难看出,可变形状的字段和嵌套记录是一种非常强大的功能。与此相比,pandas的DataFrame并不直接支持该功能,但它的分层索引机制跟这个差不多。
为什么要用结构化数组
跟pandas的DataFrame相比,NumPy的结构化数组是一种相对较低级的工具。它可以将单个内存块解释为带有任意复杂嵌套列的表格型结构。由于数组中的每个元素在内存中都被表示为固定的字节数,所以结构化数组能够提供非常快速高效的磁盘数据读写(包括内存映像,稍后将详细介绍)、网络传输等功能。
结构化数组的另一个常见用法是,将数据文件写成定长记录字节流,这是C和C++代码中常见的数据序列化手段(业界许多历史系统中都能找得到)。只要知道文件的格式(记录的大小、元素的顺序、字节数以及数据类型等),就可以用np.fromfile将数据读入内存。这种用法超出了本书的范围,只要知道有这么一回事就可以了。
结构化数组操作:numpy.lib.recfunctions
适用于结构化数组的函数没有DataFrame那么多。NumPy模块numpy.lib.recfunctions中有一些用于增删字段或执行基本连接运算的工具。对于这些工具,我们需要记住的是:一般都需要创建一个新数组以便对dtype进行修改(比如添加或删除一列)。这些函数就留给有兴趣的读者自己去研究了,因为本书中不会用到它们。