Pandas DataFrame 新增操作最佳实践

版权声明:本文为博主原创文章,未经博主允许不得转载。

1 背景

项目中需对已产生的 Pandas DataFrame 进行新增操作,想到的方法有四种:

  1. 直接使用 Pandas 的 append 方法
  2. 使用 Pandas 的 loc 方法
  3. 先转换为dict,再通过dict合并
  4. 使用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组验证实验结果如下:

在这里插入图片描述

结论如下:

  1. extend_list 即第四种计算方式(4 使用list封装,再转换为DataFrame),执行速度最快,但内存消耗多
  2. pandas_append 即第一种计算方式(1 直接使用 Pandas 的 append 方法),执行速度较慢,但内存消耗少;append调用次数为batch_size
  3. combine_dict(3 先转换为dict,再通过dict合并),执行速度与extend_list相比较慢,虽然没有使用DataFrame的新增,但实际上在转换为dict时对每个元素都进行了遍历;其内存消耗最大,是因为转换为dict后冗余了过多的字段名
  4. 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%,是该方法耗时的关键所在。

在这里插入图片描述

pandas_append

3.2 pandas_loc

模拟参数如下:

# 模拟生成大批次数据
list_size = 100
batch_size = 100

由调用关系可知, Pandas 的 loc 方法其内部使用了append。append是针对每个元素,即append调用次数为 list_size × batch_size=10000。而该方法耗时,还有一部分原因是因为,需要对DataFrame使用reindex方法进行索引的重建。

在这里插入图片描述

pandas_loc

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

在这里插入图片描述

combine_dict

3.4 extend_list

模拟参数如下:

# 模拟生成大批次数据
list_size = 200
batch_size = 10000

可知最耗时的是在生成 DataFrame 的 __ init __ 和 __ getitem __ 方法。

在这里插入图片描述

extend_list

注:@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

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值