在处理实际数据时,你是否遇到过这种情况:你有一张表格,里面既有字符串(比如姓名),又有整数(比如年龄),还有浮点数(比如考试分数)?如果我们使用标准的 NumPy 数组,由于要求数据类型必须一致,这就显得力不从心了。通常我们可能会想到使用 Pandas 的 DataFrame,但在处理底层的高性能计算或与 C 语言接口交互时,NumPy 提供了一个更轻量且强大的解决方案——结构化数组(Structured Arrays)。
在这篇文章中,我们将深入探讨 NumPy 的结构化数组。我们将学习它们如何像 C 语言的结构体一样工作,如何高效地创建它们,以及如何在实际项目中利用它们处理异构数据。我们将通过丰富的代码示例,带你从入门到精通,掌握这一强大的数据工具。
什么是结构化数组?
NumPy 的结构化数组与 C 语言中的 Struct(结构体) 非常相似。它的核心能力是允许我们将不同数据类型、不同大小的数据组织在同一个内存块中。
想象一下,普通的 NumPy 数组就像是一排排同样大小的储物柜,每个柜子只能放同样类型的物品。而结构化数组则像是复杂的档案柜,每一个“抽屉”里都有一个文件夹,里面分别放着名片(字符串)、身份证号(整数)和照片(浮点数矩阵)。
字段:数据的基本单元
结构化数组使用被称为 字段 的容器来组织数据。我们可以把结构化数组看作是一维的,但它的每个元素都是一个类似结构体的对象。每个字段都有:
- 名称:例如 ‘name‘, ‘age‘, ‘height‘。
- 数据类型:例如 INLINECODE7106bf97, INLINECODE0f5797fb,
np.float64。
结构化数组的属性
在开始编码之前,我们需要了解结构化数组的几个关键特性,这能帮助我们避免常见的陷阱:
- 字段一致性:数组中的所有记录都具有完全相同的字段集合。你不能给数组中间的某一条记录突然添加一个新字段,而其他记录没有。
- 内存布局:虽然 Python 层面帮我们做了很多封装,但在底层,结构化数组的内存是连续的。这意味着它在与 C/C++ 代码交互时非常高效。
创建结构化数组
在 Python 中,我们可以利用 NumPy 提供的多种方式来创建结构化数组。最核心的步骤是定义 数据类型。
步骤 1:定义数据类型
定义数据类型是创建结构化数组的关键。我们可以通过创建一个元组列表来实现,其中每个元组的格式为 (字段名称, 数据类型)。
步骤 2:指定形状与填充数据
定义好 dtype 后,我们可以像普通数组一样使用 np.array 来填充数据。
示例 1:基础的创建方式
让我们从一个简单的例子开始。我们要定义一个包含姓名和年龄的数组:
import numpy as np
# 定义数据类型:
# ‘name‘ 字段是最大长度为 10 的字符串
# ‘age‘ 字段是 32位整数
dt = np.dtype([(‘name‘, (np.str_, 10)), (‘age‘, np.int32)])
# 创建数组并填充数据
# 注意:这里传入的是一个包含元组的列表
students = np.array([(‘Alice‘, 18), (‘Bob‘, 19), (‘Charlie‘, 20)], dtype=dt)
print("学生数组:")
print(students)
print("
数据类型定义:")
print(students.dtype)
输出:
学生数组:
[(‘Alice‘, 18) (‘Bob‘, 19) (‘Charlie‘, 20)]
数据类型定义:
[(‘name‘, ‘<U10'), ('age', '<i4')]
实用见解:关于字符串长度的注意事项
你可能会注意到我们在定义字符串时使用了 INLINECODE789e9ed2。这里的 INLINECODE5ade8031 表示分配给该字段的最大内存长度。如果你试图存入一个超过 10 个字符的字符串,它会被静默截断。
让我们看看这个潜在的问题:
# 定义一个名字最大长度为 5 的结构
dt = np.dtype([(‘name‘, ‘U5‘), (‘score‘, ‘f4‘)])
data = np.array([(‘Alexander‘, 95.5)], dtype=dt)
print(data[‘name‘])
# 输出将会是 ‘Alexa‘,后面的 ‘nder‘ 被截断了!
最佳实践:在定义字符串字段时,一定要留出足够的余量,或者在数据预处理阶段检查字符串长度,以防数据丢失。
访问和修改数据
结构化数组的强大之处在于我们可以通过 点记法 或 字典键法 来访问特定的字段。
按字段访问
我们可以获取某一列的所有数据,这就像 Pandas 的操作一样简单:
# 接上文的 students 数组
print("所有人的姓名:", students[‘name‘])
print("所有人的年龄:", students[‘age‘])
单个元素的访问
如果你想要获取某一行(某一个记录)的数据,可以使用索引:
# 获取第一个学生的记录
first_student = students[0]
print("第一个学生记录:", first_student)
# 也可以直接获取第一个学生的姓名
print("第一个学生的姓名:", students[0][‘name‘])
添加新字段(进阶操作)
虽然不能随意改变形状,但我们可以使用 numpy.lib.recfunctions 库来给现有的结构化数组添加新字段。这在数据清洗过程中非常有用。
from numpy.lib import recfunctions as rfn
# 原始数组
data = np.array([(1, 10.5), (2, 20.5)], dtype=[(‘id‘, ‘i4‘), (‘val‘, ‘f4‘)])
# 添加一个新字段 ‘double_val‘
new_data = rfn.append_fields(data, ‘double_val‘, data=data[‘val‘] * 2, usemask=False)
print(new_data)
结构化数组的操作
Python NumPy 允许我们对整个结构化数组执行许多向量化操作。这意味着我们不需要编写缓慢的 for 循环,就可以操作整个字段的数据。
1. 排序:多级排序的艺术
我们可以使用 numpy.sort() 方法对结构数组进行排序。最棒的是,我们可以指定按哪个字段排序。这在处理“先按班级排序,再按分数排序”这类需求时非常方便。
示例:根据不同字段排序
import numpy as np
# 定义一个包含更多信息的结构
dt = np.dtype([(‘name‘, (np.str_, 10)), (‘class‘, ‘i4‘), (‘score‘, ‘f4‘)])
grades = np.array([
(‘Sana‘, 2, 85.5),
(‘Mansi‘, 7, 92.0),
(‘Rahul‘, 2, 88.0),
(‘Amit‘, 7, 85.5)
], dtype=dt)
# 1. 仅根据分数排序
sorted_by_score = np.sort(grades, order=‘score‘)
print("按分数排序:
", sorted_by_score)
# 2. 多级排序:先按班级,再按分数
# 注意:order 参数接受一个列表
sorted_multi = np.sort(grades, order=[‘class‘, ‘score‘])
print("
多级排序 (班级 -> 分数):
", sorted_multi)
输出解析:
在多级排序中,系统会先比较 INLINECODE438a8836,如果班级相同,再比较 INLINECODE8b479640。这对于生成成绩单或排行榜非常实用。
2. 统计:查找最小值和最大值
我们可以使用 INLINECODE99c04f55 和 INLINECODEc55d0b59 函数针对特定字段进行统计。
示例:统计特定字段
max_score = np.max(grades[‘score‘])
min_score = np.min(grades[‘score‘])
# 甚至可以找出谁得了最高分
# 使用布尔索引
top_student_idx = np.argmax(grades[‘score‘])
top_student = grades[top_student_idx]
print(f"最高分是: {max_score}")
print(f"最高分得主: {top_student[‘name‘]} (班级 {top_student[‘class‘]})")
常见错误提醒:
初学者常犯的错误是直接对结构化数组调用 INLINECODEa5b616e1。这会导致错误,因为 NumPy 不知道该比较字符串还是数字。你必须指定字段:INLINECODEa815ee25。
3. 组合:连接结构化数组
处理来自不同文件的数据时,我们可能需要合并它们。我们可以使用 np.concatenate() 函数。
重要条件:被连接的数组必须具有相同的结构(字段名称和数据类型必须完全一致)。
# 第一批学生
batch_a = np.array([(‘Alice‘, 18), (‘Bob‘, 19)], dtype=dt)
# 第二批学生 (注意:dtype 必须兼容)
batch_b = np.array([(‘Charlie‘, 20)], dtype=dt)
# 连接
all_students = np.concatenate((batch_a, batch_b))
print("合并后的名单:", all_students)
4. 形状变换:重塑结构化数组
我们可以使用 np.reshape() 函数来改变数组的维度。这在处理图像数据或时间序列数据时非常有用。
注意:重塑操作只是改变查看数据的方式,不会改变底层数据的顺序。
# 创建一个有 4 条记录的数组
arr = np.array([
(‘A‘, 1), (‘B‘, 2),
(‘C‘, 3), (‘D‘, 4)
], dtype=[(‘name‘, ‘U1‘), (‘val‘, ‘i4‘)])
# 将其重塑为 2x2 的矩阵
reshaped_arr = np.reshape(arr, (2, 2))
print("重塑后的数组 (2x2):")
print(reshaped_arr)
# 现在你可以通过两个维度访问数据
# 访问第 0 行第 1 列的数据
print("
第0行第1列的数据:", reshaped_arr[0, 1])
性能优化与最佳实践
虽然结构化数组非常强大,但在使用时仍需注意性能和内存占用。
1. 内存对齐与填充
为了提高 CPU 访问内存的效率,结构体中的字段通常需要根据内存边界对齐。NumPy 允许我们通过 align=True 参数来创建对齐的结构。这虽然可能会增加内存占用(因为会有填充字节),但在某些硬件上能显著提升访问速度。
# 创建一个内存对齐的结构
dt_aligned = np.dtype(
[(‘name‘, ‘U10‘), (‘age‘, ‘i4‘), (‘score‘, ‘f8‘)],
align=True
)
2. 避免频繁修改形状
频繁地使用 append_fields 或改变数组形状会涉及到内存的重新分配。如果可能,建议在创建数组时就一次性定义好所有需要的字段。
3. 结构化数组 vs Pandas DataFrame
你可能会问:“为什么不直接用 Pandas?”
- 结构化数组:更接近底层,内存占用极小,没有索引开销,适合进行数值计算、与 C 库交互或处理数百万级别的简单异构数据。
- DataFrame:功能更丰富(缺失值处理、时间序列、复杂的对齐操作),适合数据分析和探索性编程。
总结
在这篇文章中,我们探索了 NumPy 结构化数组的方方面面。我们从它的基本概念入手,学习了如何定义复杂的 dtype,并深入了解了如何通过点记法操作字段。我们不仅学会了基础的增删改查,还掌握了排序、连接和重塑等高级操作。
结构化数组是连接 Python 高级数据结构与底层高性能计算的桥梁。掌握它,能让你在处理异构数值数据时多一把锋利的“瑞士军刀”。
下一步建议
现在你已经了解了结构化数组,为什么不尝试在你的下一个项目中使用它呢?你可以尝试读取一个 CSV 文件,将其转换为结构化数组,并进行一些基础的统计计算。你会发现,对于纯粹的数值处理任务,它的速度非常惊人。如果你对内存优化感兴趣,不妨研究一下 numpy.rec 模块,它提供了对结构化数组的进一步封装。