< 数组排序 | 目录 | 使用Pandas进行数据处理 >
虽然我们的数据很多情况下都能表示成同种类的数组,但是某些情况下,这是不适用的。本小节展示了如何使用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)
我们也可以类似的创建一个复合类型的数组,只需要指定相应的dtype数据类型即可:
# 使用复合的dtype参数来创建结构化数组
data = np.zeros(4, dtype={'names':('name', 'age', 'weight'),
'formats':('U10', 'i4', 'f8')})
print(data.dtype)
[('name', '<U10'), ('age', '<i4'), ('weight', '<f8')]
这里的U10
代表着“Unicode编码的字符串,最大长度10”,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]
('Alice', 25, 55.)
# 获得最后一行的名字
data[-1]['name']
'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数据类型来指定:
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' |
原始数据 | np.dtype('V') == np.void |
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.]]
X
数组中的每个元素都有一个id
和一个$3\times 3$的矩阵。为什么需要这样用,为什么不用一个多维数组或者甚至是Python的字典呢?原因是NumPy的dtype
数据类型直接对应这一个C语言的结构体定义,因此存储这个数组的内容内容可以直接被C语言的程序访问到。如果你在写访问底层C语言或Fortran语言的Python接口的话,你会发现这种结构化数组很有用。
data['age']
array([25, 45, 37, 19], dtype=int32)
如果我们使用记录数组来展示数据化,我们可以使用对象属性方式访问年龄字段,少打几个字:
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
99.7 ns ± 0.475 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each) 1.49 µs ± 12.2 ns per loop (mean ± std. dev. of 7 runs, 1,000,000 loops each) 2.81 µs ± 26 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)
是使用更方便简洁的写法还是使用更高性能的写法,取决于你应用的需求。
< 数组排序 | 目录 | 使用Pandas进行数据处理 >