版权声明:本文为博主原创文章,未经博主允许不得转载。
1 背景
项目中需对已产生的 Pandas DataFrame 进行新增操作,想到的方法有四种:
- 直接使用 Pandas 的 append 方法
- 使用 Pandas 的 loc 方法
- 先转换为dict,再通过dict合并
- 使用list封装,再转换为DataFrame
2 性能验证
验证代码如下:
# encoding: utf-8
"""
Pandas DataFrame 新增操作避免使用append方法
:Author: dkjkls
:Create: 2019/6/16 12:03
Copyright (c) https://blog.csdn.net/dkjkls
"""
import pandas as pd
import numpy as np
from datetime import datetime
from memory_profiler import profile
@profile
def pandas_append(df_list):
"""
1 直接使用 Pandas 的 append 方法
Author: dkjkls
Blog: https://blog.csdn.net/dkjkls
Create: 2019/6/16 21:46
"""
start = datetime.now()
df_result = pd.DataFrame()
for df in df_list:
df_result = df_result.append(df)
print('pandas_append 耗时:%s秒' % (datetime.now() - start))
return df_result
@profile
def pandas_loc(df_list):
"""
2 使用 Pandas 的 loc 方法
Author: dkjkls
Blog: https://blog.csdn.net/dkjkls
Create: 2019/6/16 21:46
"""
start = datetime.now()
df_result = pd.DataFrame(columns=['product', 'order', 'time', 'location', 'type'])
for df in df_list:
for index, row in df.iterrows():
df_result.loc[df_result.shape[0] + 1] = row
print('pandas_loc 耗时:%s秒' % (datetime.now() - start))
return df_result
@profile
def combine_dict(df_list):
"""
3 先转换为dict,再通过dict合并
Author: dkjkls
Blog: https://blog.csdn.net/dkjkls
Create: 2019/6/16 21:46
"""
start = datetime.now()
dict_list = [df.to_dict() for df in df_list]
combine_dict = {}
i = 0
for dic in dict_list:
length = len(list(dic.values())[0])
for idx in range(length):
combine_dict[i] = {k: dic[k][idx] for k in dic.keys()}
i += 1
df_result = pd.DataFrame.from_dict(combine_dict, 'index')
print('combine_dict 耗时:%s秒' % (datetime.now() - start))
return df_result
@profile
def extend_list(df_list):
"""
4 使用list封装,再转换为DataFrame
Author: dkjkls
Blog: https://blog.csdn.net/dkjkls
Create: 2019/6/16 21:46
"""
start = datetime.now()
product_list = []
order_list = []
time_list = []
location_list = []
type_list = []
for df in df_list:
product_list.extend(df['product'])
order_list.extend(df['order'])
time_list.extend(df['time'])
location_list.extend(df['location'])
type_list.extend(df['type'])
df_result = pd.DataFrame({'product': product_list, 'order': order_list, 'time': time_list,
'location': location_list, 'type': type_list})
print('extend_list 耗时:%s秒' % (datetime.now() - start))
return df_result
if __name__ == '__main__':
# 模拟生成大批次数据
list_size = 100
batch_size = 10000
df_list = [pd.DataFrame({
'product': [np.random.rand() for _ in range(list_size)],
'order': [np.random.rand() for _ in range(list_size)],
'time': [np.random.rand() for _ in range(list_size)],
'location': [np.random.rand() for _ in range(list_size)],
'type': [np.random.rand() for _ in range(list_size)]
}) for i in range(batch_size)]
# 1 直接使用 Pandas 的 append 方法
df_result_1 = pandas_append(df_list)
# 2 使用 Pandas 的 loc 方法
df_result_2 = pandas_loc(df_list)
# 3 先转换为dict,再通过dict合并
df_result_3 = combine_dict(df_list)
# 4 使用list封装,再转换为DataFrame
df_result_4 = extend_list(df_list)
模拟生成大批次数据,list_size为每一个DataFrame中行数大小,batch_size为要新增的DataFrame个数。
6组验证实验结果如下:
结论如下:
- extend_list 即第四种计算方式(4 使用list封装,再转换为DataFrame),执行速度最快,但内存消耗多
- pandas_append 即第一种计算方式(1 直接使用 Pandas 的 append 方法),执行速度较慢,但内存消耗少;append调用次数为batch_size
- combine_dict(3 先转换为dict,再通过dict合并),执行速度与extend_list相比较慢,虽然没有使用DataFrame的新增,但实际上在转换为dict时对每个元素都进行了遍历;其内存消耗最大,是因为转换为dict后冗余了过多的字段名
- pandas_loc(2 使用 Pandas 的 loc 方法)其内部使用了append,且是对每个元素都进行遍历,新增是针对每个元素,即append调用次数为 list_size × batch_size,因此耗时最长
3 底层揭秘
3.1 pandas_append
Pandas 的 append 方法注释中有两句话:
Append rows of `other` to the end of this frame, returning a new object.
Iteratively appending rows to a DataFrame can be more computationally intensive than a single concatenate. A better solution is to append those rows to a list and then concatenate the list with the original DataFrame all at once.
意思是,append 方法只会返回一个新的对象,而不是在原来的DataFrame中新增;
对于迭代新增行,官方的建议是先统一添加到一个list中,再和原始DataFrame进行一次添加。
至于 append 为何会很慢,可以查看调用关系,模拟参数如下:
# 模拟生成大批次数据
list_size = 200
batch_size = 10000
可通过 PyCharm 的 Profile 运行代码,产出 Statistics 和 Call graph,查看各方法耗时、占比、调用次数、调用关系。
如下 Statistics 可知,append 方法耗时78.8%,调用次数为batch_size = 10000;从 Call graph 调用关系可知,append 方法逐层最后会从两条线调用 numpy.core.multiarray.concatenate,该方法耗时74.7%,是该方法耗时的关键所在。
3.2 pandas_loc
模拟参数如下:
# 模拟生成大批次数据
list_size = 100
batch_size = 100
由调用关系可知, Pandas 的 loc 方法其内部使用了append。append是针对每个元素,即append调用次数为 list_size × batch_size=10000。而该方法耗时,还有一部分原因是因为,需要对DataFrame使用reindex方法进行索引的重建。
3.3 combine_dict
模拟参数如下:
# 模拟生成大批次数据
list_size = 200
batch_size = 10000
可知最耗时的是 DataFrame 的 from_dict 方法,该方法耗时79.0%,再往下层看,_from_nested_dict耗时73.6%,是该方法耗时的关键所在。本文所使用的的方式是from_dict中的 ‘index’ ,即每一个key表示一行。
pd.DataFrame.from_dict(combine_dict, 'index')
继续撸源码,突然发现一个 todo:this should be seriously cythonized,意思是需要用Cython实现。再看看代码,是对传入的dict进行遍历,不仅是遍历每行数据,还要遍历每个字段,这效率低的吓人。
对于大数据量的转换,还是不要碰 DataFrame 的 from_dict(orient=‘index’)方法了。
def _from_nested_dict(data):
# TODO: this should be seriously cythonized
new_data = OrderedDict()
for index, s in compat.iteritems(data):
for col, v in compat.iteritems(s):
new_data[col] = new_data.get(col, OrderedDict())
new_data[col][index] = v
return new_data
3.4 extend_list
模拟参数如下:
# 模拟生成大批次数据
list_size = 200
batch_size = 10000
可知最耗时的是在生成 DataFrame 的 __ init __ 和 __ getitem __ 方法。
注:@profile 是 memory_profiler 中的注解,用来获得方法使用内存详情,但会消耗计算时间,在获得计算时间时需把注解去掉。内存详情如下:
Filename: ~/test/pandas_append_test.py
Line # Mem usage Increment Line Contents
================================================
63 963.0 MiB 963.0 MiB @profile
64 def extend_list(df_list):
65 """
66 4 使用list封装,再转换为DataFrame
67 """
68 963.0 MiB 0.0 MiB start = datetime.now()
69 963.0 MiB 0.0 MiB product_list = []
70 963.0 MiB 0.0 MiB order_list = []
71 963.0 MiB 0.0 MiB time_list = []
72 963.0 MiB 0.0 MiB location_list = []
73 963.0 MiB 0.0 MiB type_list = []
74
75 1009.4 MiB 0.0 MiB for df in df_list:
76 1009.4 MiB 0.7 MiB product_list.extend(df['product'])
77 1009.4 MiB 0.7 MiB order_list.extend(df['order'])
78 1009.4 MiB 0.7 MiB time_list.extend(df['time'])
79 1009.4 MiB 0.7 MiB location_list.extend(df['location'])
80 1009.4 MiB 0.7 MiB type_list.extend(df['type'])
81
82 1009.4 MiB 0.0 MiB df_result = pd.DataFrame({'product': product_list, 'order': order_list, 'time': time_list,
83 1047.6 MiB 38.1 MiB 'location': location_list, 'type': type_list})
84
85 1047.6 MiB 0.0 MiB print('extend_list 耗时:%s秒' % (datetime.now() - start))
86 1047.6 MiB 0.0 MiB return df_result
--------------------------文档信息--------------------------
版权声明:本文为博主原创文章,未经博主允许不得转载
署名(BY) :dkjkls(dkj卡洛斯)
文章出处:http://blog.csdn.net/dkjkls