把文件里的多列数值按某一列模式拆开后合并成同一个数组,在传感器记录、实验日志和报表导出数据中都很常见。以逗号分隔的文本文件为例,一行通常包含一个模式标识和三列数值,比如 mode,a,b,c。需求是读取文件后,把属于同一模式的 a、b、c 三列连续展平成一维数组,再按模式顺序拼接到同一个数组里。这个操作如果用逐行循环实现,代码容易写但解析开销很大,尤其是浮点转换和列表扩容都发生在 Python 层。

接下来先从低效写法入手,再分别用 NumPy 和 pandas 给出两种更高效的实现,并讨论大文件分块读取时的注意点。
逐行循环为什么慢
最常见的做法是打开文件、跳过表头,然后对每一行执行 split、float 转换,再把数值追加到列表。下面这段代码逻辑直观,但在数据量达到数万行以上时会明显变慢。
from collections import defaultdict
groups = defaultdict(list)
with open('data.txt', 'r', encoding='utf-8') as f:
next(f) # 跳过表头
for line in f:
parts = line.strip().split(',')
mode = parts[0]
values = [float(x) for x in parts[1:]]
groups[mode].extend(values)
result = []
for mode in sorted(groups):
result.extend(groups[mode])
import numpy as np
arr = np.array(result, dtype=np.float64)
这段代码的主要瓶颈不在读取本身,而在 Python 解释器需要为每行创建多个字符串对象,并逐个调用 float 转换。列表不断 extend 时还会触发多次内存重分配。对 20 万行数据做一次简单测试,纯循环通常需要数秒才能完成,而同样数据交给 NumPy 或 pandas 处理往往不到 0.1 秒。
另一个隐藏问题是结果数组的精度和内存占用。逐行追加时,如果一开始没有固定 dtype,列表里可能混入 Python float 对象,最终转成 NumPy 数组时还要复制一整份数据。理想做法是让底层 C 解析函数直接生成连续内存块,把循环次数从行级降到组级。
用 NumPy 高效读取并分组展平
如果文件里的模式列已经是整数编码,并且所有列都是数值,那么 np.loadtxt 可以直接把数据读成二维数组。随后用布尔掩码按模式筛选,再对每个模式对应的子矩阵调用 ravel,最后用 np.concatenate 合并。这样循环只执行模式个数次,真正耗时的筛选和展平都在 C 层完成。
import numpy as np
# 假设 mode 列已经用 0、1 等整数编码,所有列都是数值
data = np.loadtxt('data.txt', delimiter=',', skiprows=1)
modes = data[:, 0].astype(np.int32)
numeric = data[:, 1:]
mode_order = np.unique(modes)
chunks = []
for mode in mode_order:
mask = modes == mode
flat = numeric[mask].ravel()
chunks.append(flat)
arr = np.concatenate(chunks)
这里要特别注意 ravel 和 flatten 的区别。ravel 在数据本身连续时返回视图,不复制数据;而 flatten 总是返回新的副本。对每个分组来说,筛选得到的 numeric[mask] 本身就是新数组,调用 ravel 通常只需拿到连续的一维视图,开销比 flatten 更低。当然,最后合并时 np.concatenate 会一次性分配目标数组,这是无法避免的复制。
如果模式列是字符串,np.loadtxt 读取混合类型并不方便,通常会退回较慢的 np.genfromtxt。实际项目中更推荐让 pandas 承担 CSV 解析,再用 to_numpy 转成 NumPy 数组。pandas 的 C 解析引擎对混合类型列处理得更好,速度也接近纯数值读取。
让 pandas 承担解析,分组后展平
pandas 的 read_csv 默认使用 C 引擎解析 CSV 文件,对字符串模式列和多列浮点数都有很好的支持。读取后调用 groupby 按模式列分组,每个分组的 to_numpy 可以直接返回指定 dtype 的二维数组,再通过 ravel 展平。代码比纯 NumPy 路线更简洁,同时保持了很高效率。
import pandas as pd
import numpy as np
df = pd.read_csv('data.txt', sep=',')
chunks = []
for mode, group in df.groupby('mode', sort=True):
flat = group[['a', 'b', 'c']].to_numpy(dtype=np.float64).ravel()
chunks.append(flat)
arr = np.concatenate(chunks)
这里 sort=True 会让分组结果按模式字符串排序,保证输出数组顺序稳定。如果不想排序,可以设置 sort=False,分组顺序取决于模式列在文件中首次出现的顺序。对于不需要顺序保证的场景,关闭排序可以减少一些开销。
当文件很大、无法一次性载入内存时,应该使用 chunksize 分块读取。分块后如果直接在每个块内部做 groupby,同一模式可能被切到不同块中,导致最终拼接顺序被打乱。此时可以用一个字典把每个模式在多个块里的扁平数组先收集起来,最后按模式顺序合并。
import pandas as pd
import numpy as np
buckets = {}
for part in pd.read_csv('data.txt', sep=',', chunksize=50000):
for mode, group in part.groupby('mode', sort=False):
flat = group[['a', 'b', 'c']].to_numpy(dtype=np.float64).ravel()
buckets.setdefault(mode, []).append(flat)
arr = np.concatenate([np.concatenate(buckets[m]) for m in buckets])
这种写法在每一块结束后立即把数值数组交给 buckets,不会在循环中保留完整 DataFrame,内存占用基本由 chunksize 和最终结果数组决定。如果数据文件包含缺失值,可以在 read_csv 中指定 na_values 或用 dtype 明确浮点类型,避免 pandas 自动推断成 object 类型,否则 to_numpy 会产生 Python 对象数组,性能会明显下降。
性能对比与选型建议
为了更直观地比较,可以生成一份 20 万行、三个模式、三列浮点数的模拟数据,分别测试逐行循环、NumPy 布尔掩码和 pandas 分组方案。测试代码大致如下。
import numpy as np
import pandas as pd
import time
rng = np.random.default_rng(42)
n = 200000
modes = rng.choice(['A', 'B', 'C'], size=n)
data = rng.normal(size=(n, 3))
df = pd.DataFrame({'mode': modes, 'a': data[:, 0], 'b': data[:, 1], 'c': data[:, 2]})
df.to_csv('data.txt', index=False)
start = time.perf_counter()
chunks = []
for mode, group in pd.read_csv('data.txt').groupby('mode', sort=True):
chunks.append(group[['a', 'b', 'c']].to_numpy(dtype=np.float64).ravel())
arr = np.concatenate(chunks)
print(f'耗时 {time.perf_counter() - start:.3f} 秒,数组长度 {len(arr)}')
在常规的消费级 CPU 上,逐行循环处理 20 万行通常需要数秒;pandas 方案一般可以在 0.1 秒左右完成;纯 NumPy 布尔掩码如果文件已经是数值矩阵,通常也能做到相近数量级。分块读取会略微增加 Python 循环和字典操作成本,但换来的是内存可控,适合千万行级别的文件。
实际选型时,如果数据规模在内存范围以内、模式列是字符串,优先使用 pandas 一次性读取再分组展平,代码最少、出错率低。如果所有列都是数值且模式列已经编码,可以直接使用 np.loadtxt,避免引入 pandas 依赖。若是超大文件,则用分块读取加字典收集,必要时可以把最终数组写入 .npy 文件,避免重复解析。
高效导入多列数据并展平的核心思路,是把逐行的字符串解析和浮点转换交给 C 实现,把循环次数从行级降到模式级。展平时尽量选择 ravel 而非 flatten,并控制好 dtype,避免 object 类型。掌握这些技巧后,即使面对百万行文件,也能在很短的时间内得到布局整齐的一维数组。
Python数组导入数据分组展平numpy高效读取修改时间:2026-10-03 16:51:02