广告

数组提取指定列的高效方法:5 种常用技巧与性能对比(NumPy/Pandas 实例)

在处理大规模数据时,快速提取数组中的某些列是常见的性能瓶颈。 本文整理了 5 种常用技巧,聚焦 NumPy 与 Pandas 的实现与对比,以帮助你在实际场景中实现高效提取。随后给出具体示例,帮助你在项目中直接落地应用。

直接切片与连续列选择

连续区间切片的要点

连续列的切片是最省力也最高效的方法,当目标列在数据中的位置形成一个连续区间时,可以直接用切片语法获得所需子矩阵,避免额外的索引开销。

NumPy 的切片语法非常接近数组的结构,易于实现并且内存使用低,当目标列恰好是连续区间时,复制量最小,吞吐量最高。

import numpy as np# 假设 a 的形状为 (N, M)
start, end = 2, 5          # 选择列 2,3,4
sub = a[:, start:end]      # 直接切片,输出形状为 (N, end-start)

Pandas 版本也同样高效,尤其是按位置切片时,可以使用 .iloc 实现连续列的快速提取。

import pandas as pd# 假设 df 是一个 DataFrame,按位置切片选择连续列
sub_df = df.iloc[:, 2:5]  # 选择列索引 2,3,4

在性能基准中,连续区间的切片往往是最快的选项,因为它避免了逐列的随机访问和额外的拷贝。若你的数据列恰好构成一个连续区间,这是首选方案。

基准对比示例(时间单位:毫秒,单次测量):

import timeitsetup = """
import numpy as np
a = np.random.rand(10000, 200)
"""
stmt = "a[:, 2:5]"
print(timeit.timeit(stmt, setup=setup, number=1000))

要点总结:若目标列为连续区间,优先使用直接切片;对 Pandas 来说,.iloc 的连续切片具有与 NumPy 相似的高效。

数组提取指定列的高效方法:5 种常用技巧与性能对比(NumPy/Pandas 实例)

使用 np.take 或 take_along_axis 提取非连续列

非连续列的索引提取

当需要提取的列呈非连续分布时, NumPy 的 take 或 take_along_axis 能有效实现按列索引的聚合提取,避免逐列循环的成本。

take 直接按列索引提取,输出为新的数组,适用于一次性获取多列、且不需要保持原始数据的顺序时的快速实现。

import numpy as np# 假设 a 的形状为 (N, M),需要提取的列索引
indices = [1, 4, 7]
sub = np.take(a, indices, axis=1)  # 形状 (N, len(indices))

take_along_axis 提供对齐的能力,适合需要保持某些维度对齐的场景,可以灵活处理不同形状的索引数组。

import numpy as npindices = np.array([1, 4, 7])
ix = indices[np.newaxis, :]  # 形状 (1, k)
sub = np.take_along_axis(a, ix, axis=1)  # 形状 (N, k)

Pandas 侧的等效实现,可以直接通过位置索引获得对应的列集合:

# Pandas 的等效:按位置选择列
sub_df = df.take([1, 4, 7], axis=1)

在非连续列场景下,np.take 系列方法通常具备良好的性能,尤其当列数较多且需要跨越数据的多个区域时。

示例对比(NumPy vs Pandas):在相同数据量下,take/iloc 的性能差异往往与内存拷贝和索引开销相关,具体值随数据分布和数组对齐而变化。

要点总结:遇到非连续列时,优先考虑 np.take 或 np.take_along_axis 来完成一次性提取;Pandas 侧使用 .take 或 .iloc/重新索引也能实现高效获取。

布尔掩码与高效索引组合

布尔掩码的列选择方案

布尔掩码可以提供灵活的选择方式,当你需要基于条件动态筛选列时,布尔掩码成为一个强大工具。

使用布尔掩码的一个常见实现是通过列数量的布尔向量来筛选,例如 mask 的 True 位置对应需要保留的列。

import numpy as np# 假设 a 的形状为 (N, M),需要选取的列为 [1, 3, 5]
mask = np.zeros(a.shape[1], dtype=bool)
mask[[1, 3, 5]] = True
sub = a[:, mask]

也可以借助 np.compress 按列进行筛选,在对大量列进行条件筛选时,可能会带来更好的可读性和一致性。

sub = np.compress(mask, a, axis=1)

Pandas 的实现也非常直观,通过布尔向量筛选或基于条件计算得到的布尔列选择集合即可:

cols = df.columns[mask]
sub_df = df[cols]

注意点:布尔掩码通常会产生新的复制,若你关注内存开销,可能需要结合前两种方法并对掩码的产生方式进行优化。

在实际应用中,布尔掩码提供了极好的灵活性,尤其是在列分布与取值条件动态变化的场景中,可以快速扩展到不同的列集合。

Pandas 列选择的简洁高效

通过标签进行快速列提取

Pandas 的列选择对标签友好,语义清晰,df.loc 允许按标签选择多列,且可结合切片实现高效数据子集化。

使用 df.loc[:, cols] 能确保你拿到一个新的 DataFrame,列的顺序与 cols 保持一致,同时保持读取的直观性。

import pandas as pd# 通过标签选择列
cols = ['col_a', 'col_b', 'col_c']
sub_df = df.loc[:, cols]

另一种常用的方式是 filter,它在需要动态生成列集合时更具表达力

sub_df = df.filter(items=cols)

若后续需要进行数值计算,可将结果转换为 NumPy 数组,以便在底层进行向量化运算:

sub_arr = sub_df.to_numpy()

要点总结:Pandas 的标签驱动选择提供了简洁的写法和清晰的语义,适合以列名为主的工作流;在需要快速子集化时,使用 loc 与 filter 能保持代码的可读性与稳定性。

内存布局与输出缓冲区的优化

预分配输出缓冲区以减少拷贝

预分配输出缓冲区是提升重复提取性能的核心技巧,尤其在需要多次重复提取相同结构的列时,可以显著减少动态分配的成本。

通过 out 参数将结果直接写入事先分配的数组,可以避免中间拷贝,提升向量化提取的吞吐量。

import numpy as np# 需要提取的列
indices = [2, 3, 7]
n, _ = a.shape
out = np.empty((n, len(indices)), dtype=a.dtype)
np.take(a, indices, axis=1, out=out)
sub = out

确保输入数据是连续内存布局可以提升性能,对两维数组,先将其转换为 C 连续的形式通常有益于访问效率。

# 将输入数组转换为 C 连续
a_contig = np.ascontiguousarray(a)

Pandas 的做法则是尽量让操作保持在高效路径上,如需更高性能的内存控制,可以在数据帧层面进行列的重新排序或再索引,以确保后续处理的局部性。

# 在 Pandas 中,可以通过 reindex 确保列顺序一致,提升后续处理的局部性
df2 = df.reindex(columns=[...])

要点总结:使用预分配输出缓冲区和确保连续内存布局,是实现大规模列提取高吞吐的关键手段;结合 NumPy 的 out 参数和数组布局优化,可以明显降低重复提取的成本。

在基准测试的设置中,我们也会引入温度参数,例如 temperature=0.6,用以模拟不同输入模式对提取性能的影响,从而对比方法的鲁棒性与稳定性。尽管这并不改变算法本身的逻辑,它帮助评估在变动条件下的实际表现。

广告

后端开发标签