Pandas DataFrame数据处理性能深度评测:iloc、at、loc与底层数组提取效率对比
本文针对500万行级别的大规模数据,系统对比了Pandas中iloc、at、apply及Series直接处理的速度差异。实测显示,iloc耗时约5小时,at优化至10分钟,而转为Series处理仅需37秒。此外,文章通过数值实验深入剖析了loc、at、iloc、iat及直接访问values数组在读取与修改场景下的性能表现,指出直接获取二维数组再定位是最高效方案,并提供了相应的代码实现与优化建议。
数据操作的核心通常涉及增加、删除、修改及差异计算,本文旨在对比几种常见写法在处理大量数据时产生的显著时间差异。
测试数据规模约为500万行,对应文件大小为100M。
1. iloc方法
iloc是一种速度极慢的操作方式。以下示例展示了如何对每个CSV文件中的每一行进行循环,并使用iloc进行处理:
for index in range(len(df)):
df.iloc['attr'][index] = xxx
使用该方法处理500万行数据大约需要5个小时,效率非常低。
2. at方法
at方法相比iloc有显著的性能提升,同样基于for循环处理,具体示例代码如下:
for i in range(len(df)):
if df.at[i,'attr'] > 0:
sum_positive += df.at[i,'attr']
else:
sum_negetive += df.at[i,'sttr']
在我的程序环境中,at和iloc可以互换使用。采用at方法后,程序速度大幅提升至约10分钟,但仍未达到理想状态。
3. apply(lambda x:...)方法
考虑到for循环速度过慢,我尝试通过apply函数进行优化。网上有观点认为apply能大幅提升速度,但经过实际测试,在我的程序中,apply的性能与for循环相差无几,整体表现一般。
4. 直接使用Series处理
这才是真正优化for循环的有效途径。以上述at方法的程序为例,可改写为:
sum_positive += df['attr'][df.attr > 0].sum()
sum_negative += df['attr'][df.attr < 0].sum()
将程序全部改为Series处理后,运行速度显著加快。最终处理500万行数据仅需约37秒,基本符合预期目标。
此处简要讨论DataFrame的属性筛选,即上述df.attr > 0部分。Pandas的属性筛选功能强大且便捷。
其次,在进行属性筛选时,应避免直接修改属性本身,而应修改比较对象。例如,不建议这样编写代码:
float(df.attr )> 0
而应改为:df.attr > str(0),因为df.attr作为属性对象不宜随意变动。
补充:Pandas中DataFrame单个数据提取与修改效率分析
目标
在使用Pandas处理金融数据及建模时,常需按时间序列顺序遍历DataFrame数据,读取或修改特定位置的数据。经验表明,此类操作比直接处理二维列表或np.array格式数据慢得多,原因可能涉及index及columns层级的查找(两个字典结构,非连续数组,每次定位均需耗时)以及DataFrame内部数据的内存布局,后续有机会将深入研究。
此处进行一组数值实验,以对比几种方法的效率。
生成测试数据
首先生成一个包含随机数的二维数组作为DataFrame数据,为具有一般性,将列名和行名设置为标记顺序的字符串。
import numpy as np
import pandas as pd
from copy import deepcopy
from time import time
np.random.seed(20000)
I = 900
df = pd.DataFrame(np.random.standard_normal((I, I)),
columns=['c'+str(_) for _ in range(I)],
index=['i'+str(_) for _ in range(I)])
随后在限定范围内随机生成取值位置,为便于对比,将随机坐标与字符串名称对应起来。
columns_num = np.floor(np.random.uniform(0, 1, I) * I).astype(int)
index_num = np.floor(np.random.uniform(0, 1, I) * I).astype(int)
columns_str = ['c'+str(_) for _ in columns_num]
index_str = ['i'+str(_) for _ in index_num]
读取性能测试
首先采用传统方法,直接通过columns及index中的名称进行定位:
t0 = time()
for m in columns_str:
for n in index_str:
c = df[m][n]
print(time()-t0)
6.789840459823608
先获取columns列名,再在values中取行坐标,速度有所提升:
t0 = time()
for m in columns_str:
for n in index_num:
c = df[m].values[n]
print(time()-t0)
1.9697318077087402
使用loc方法,其速度与直接通过columns及index名称定位相当:
t0 = time()
for m in columns_str:
for n in index_str:
c = df.loc[n, m]
print(time()-t0)
5.661889314651489
使用at方法,比loc稍快,因为loc支持切片操作:
t0 = time()
for m in columns_str:
for n in index_str:
c = df.at[m, n]
print(time()-t0)
3.3770089149475098
假设已知具体横纵坐标,我们进一步比较:
先从values开始取值,速度依然较慢,看来每次从df中提取values非常耗时:
t0 = time()
for m in columns_num:
for n in index_num:
c = df.values[n][m]
print(time()-t0)
6.041872024536133
尝试使用iloc,结果无明显区别:
t0 = time()
for m in columns_num:
for n in index_num:
c = df.iloc[n, m]
print(time()-t0)
6.103677034378052
使用iat进行对比,提升幅度不大,令人稍感失望:
t0 = time()
for m in columns_num:
for n in index_num:
c = df.iat[n, m]
print(time()-t0)
4.375299692153931
最后,最高效的方法仍是先获取二维数组再进行定位:
t0 = time()
b = df.values
for m in columns_num:
for n in index_num:
c = b[n][m]
print(time()-t0)
0.6402544975280762
修改性能测试
重复上述过程,将对应值修改为0作为简单测试,注意需备份原始数据:
通过columns及index中的名称进行定位:
df_backup = deepcopy(df)
t0 = time()
for m in columns_str:
for n in index_str:
df_backup[m][n] = 0.0
print(time()-t0)
41.99269938468933
先获取columns列名,再在values中取行坐标:
df_backup = deepcopy(df)
t0 = time()
for m in columns_str:
for n in index_num:
df_backup[m].values[n] = 0.0
print(time()-t0)
2.215076208114624
使用loc方法:
df_backup = deepcopy(df)
t0 = time()
for m in columns_str:
for n in index_str:
df_backup.loc[n, m] = 0.0
print(time()-t0)
134.39290761947632
使用at方法,在修改数值方面竟然比loc快很多:
df_backup = deepcopy(df)
t0 = time()
for m in columns_str:
for n in index_str:
df_backup.at[n, m] = 0.0
print(time()-t0)
4.7453413009643555
在values上直接修改,效果也不错,与读取速度相近,看来主要耗时仍在于每次提取values:
df_backup = deepcopy(df)
t0 = time()
for m in columns_num:
for n in index_num:
df.values[n][m] = 0.0
print(time()-t0)
6.346027612686157
使用iloc方法:
df_backup = deepcopy(df)
t0 = time()
for m in columns_num:
for n in index_num:
df.iloc[n, m] = 0.0
print(time()-t0)
122.33384037017822
使用iat方法:
df_backup = deepcopy(df)
t0 = time()
for m in columns_num:
for n in index_num:
df.iat[n, m] = 0.0
print(time()-t0)
5.381632328033447
先获取二维数组再进行定位:
df_backup = deepcopy(df)
t0 = time()
b = df.values
for m in columns_num:
for n in index_num:
c = b[n][m]
print(time()-t0)
0.4298992156982422
总结
从效率角度看,直接获取数值是最优方案,此次系统性比较旨在记录。代码编写略显繁琐,但便于复制实验。在建模级别的代码中,我倾向于使用第2种方法,主要基于代码的可读性、维护性和修改便利性。代码中的key能明确指示数据含义,直观易读。
此前为提高运行效率,也曾尝试先提取二维数组,但当columns较多时操作繁琐,重新读取还需转换。当然,也可将数据封装为类,但感觉与Pandas融合度不佳,从建模和研究效率来看,尚未找到完美解决方案。后续将寻找时间深入研究DataFrame内部机制。
以上为个人经验总结,希望能提供参考,也希望大家多多支持本站。如有错误或未考虑周全之处,欢迎批评指正。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













