Python Pandas 遍历DataFrame的正确姿势 速度提升一万倍

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

Python Pandas 遍历DataFrame的正确姿势 速度提升一万倍


)


前言

提示:这里可以添加本文要记录的大概内容:

例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。


提示:以下是本篇文章正文内容,下面案例可供参考

一、pandas是什么?

示例:pandas 是基于NumPy 的一种工具,该工具是为了解决数据分析任务而创建的。

二、使用步骤

1.引入库

代码如下(示例):

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
import  ssl
ssl._create_default_https_context = ssl._create_unverified_context

2.读入数据

代码如下(示例):

Python Pandas 遍历DataFrame的正确姿势 速度提升一万倍
Enrique Juan
Enrique Juan
中國人
155 人赞同了该文章
最近做科研时经常需要遍历整个DataFrame,进行各种列操作,例如把某列的值全部转成pd.Timestamp格式或者将某两列的值进行element-wise运算之类的。大数据的数据量随便都是百万条起跳,如果只用for循环慢慢撸,不仅浪费时间也没效率。在一番Google和摸索后我找到了遍历DataFrame的至少8种方式,其中最快的和最慢的可以相差12000倍!

本文以相加和相乘两种操作为例,测试8种方法的运行速度,并附上示范代码。

测试环境
Macbook Pro Retina with TouchBar (13inch, 2018) i5 8GB 512GB
OS: macOS Catalina 10.5.2
Python 3.7.5 (default, Nov  1 2019, 02:16:23)
[Clang 11.0.0 (clang-1100.0.33.8)] on darwin
示范用数据
本来想造100万笔的,但是100万笔跑%timeit要跑很久,最后造了3000笔,己经足以体现运行速度差异。为了避免快取影响,每个子实验进行前都会用这个代码重造数据。

import pandas as pd
import numpy as np

# 生成樣例數據
def gen_sample():
    aaa = np.random.uniform(1,1000,3000)
    bbb = np.random.uniform(1,1000,3000)
    ccc = np.random.uniform(1,1000,3000)
    ddd = np.random.uniform(1,1000,3000)
    return pd.DataFrame({'aaa':aaa,'bbb':bbb, 'ccc': ccc, 'ddd': ddd, 'eee': None})

aaa、bbb是本文要操作的对象;ccc、ddd不会被操作到,只是要增大数据框的大小,模拟读入整个数据框和只读取aaa、bbb两列的速度差别;eee用来存放结果
实验1 - 两列元素相加
# aaa + bbb
# python 循環 + iloc 定位
def method0_sum(DF):
    for i in range(len(DF)):
        DF.iloc[i,4] = DF.iloc[i,0] + DF.iloc[i,1]

# python 循環 + iat 定位
def method1_sum(DF):
    for i in range(len(DF)):
        DF.iat[i,4] = DF.iat[i,0] + DF.iat[i,1]

# pandas.DataFrame.iterrows() 迭代器
def method2_sum(DF):
    for index, rows in DF.iterrows():
        rows['eee'] = rows['aaa'] + rows['bbb']

# pandas.DataFrame.apply 迭代
def method3_sum(DF):
    DF['eee'] = DF.apply(lambda x: x.aaa + x.bbb, axis=1)

# pandas.DataFrame.apply 迭代 + 只讀兩列
def method4_sum(DF):
    DF['eee'] = DF[['aaa','bbb']].apply(lambda x: x.aaa + x.bbb, axis=1)
    
# 列表構造
def method5_sum(DF):
    DF['eee'] = [ a+b for a,b in zip(DF['aaa'],DF['bbb']) ]

# pandas 數組操作
def method6_sum(DF):
    DF['eee'] = DF['aaa'] + DF['bbb']

# numpy 數組操作
def method7_sum(DF):
    DF['eee'] = DF['aaa'].values + DF['bbb'].values
实验1 结果
df = gen_sample()
%timeit method0_sum(df)
df = gen_sample()
%timeit method1_sum(df)
df = gen_sample()
%timeit method2_sum(df)
df = gen_sample()
%timeit method3_sum(df)
df = gen_sample()
%timeit method4_sum(df)
df = gen_sample()
%timeit method5_sum(df)
df = gen_sample()
%timeit method6_sum(df)
df = gen_sample()
%timeit method7_sum(df)

2.06 s ± 140 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
56.9 ms ± 1.18 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
358 ms ± 24.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
93 ms ± 1.57 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
90.3 ms ± 1.16 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
1.15 ms ± 39.8 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
273 µs ± 21.6 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
130 µs ± 2.91 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
实验2 - 两列元素相乘
# aaa * bbb
# python 循環 + iloc 定位
def method0_times(DF):
    for i in range(len(DF)):
        DF.iloc[i,4] = DF.iloc[i,0] * DF.iloc[i,1]

# python 循環 + iat 定位
def method1_times(DF):
    for i in range(len(DF)):
        DF.iat[i,4] = DF.iat[i,0] * DF.iat[i,1]

# pandas.DataFrame.iterrows() 迭代器
def method2_times(DF):
    for index, rows in DF.iterrows():
        rows['eee'] = rows['aaa'] * rows['bbb']

# pandas.DataFrame.apply 迭代
def method3_times(DF):
    DF['eee'] = DF.apply(lambda x: x.aaa * x.bbb, axis=1)

# pandas.DataFrame.apply 迭代 + 只讀兩列
def method4_times(DF):
    DF['eee'] = DF[['aaa','bbb']].apply(lambda x: x.aaa * x.bbb, axis=1)
    
# 列表構造
def method5_times(DF):
    DF['eee'] = [ a*b for a,b in zip(DF['aaa'],DF['bbb']) ]

# pandas 數組操作
def method6_times(DF):
    DF['eee'] = DF['aaa'] * DF['bbb']

# numpy 數組操作
def method7_times(DF):
    DF['eee'] = DF['aaa'].values * DF['bbb'].values
实验2 结果
df = gen_sample()
%timeit method0_times(df)
df = gen_sample()
%timeit method1_times(df)
df = gen_sample()
%timeit method2_times(df)
df = gen_sample()
%timeit method3_times(df)
df = gen_sample()
%timeit method4_times(df)
df = gen_sample()
%timeit method5_times(df)
df = gen_sample()
%timeit method6_times(df)
df = gen_sample()
%timeit method7_times(df)

2.04 s ± 78.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
58.4 ms ± 3.29 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
342 ms ± 8.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
89.1 ms ± 1.59 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
90.7 ms ± 769 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
1.1 ms ± 19.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
263 µs ± 11 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
131 µs ± 3.35 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
速度比较
我把结果可视化,方便比较运行速度。(顺便练习画图)


可以看到最快的数组操作和最慢的for+iloc相比差了将近万倍,是秒级和微秒级的差别。所有方法运行速度大小关系如下(由慢至快):for循环+iloc < pd.iterrows < for循环+at < pd.apply pd列表构造 = np列表构造 < pd数组操作 < np数组操作。pd和np列表构造的速度几乎一样,np列表构造略快一些(1.15毫秒和1.09毫秒的差别),所以实验只做pd列表构造。

把两个秒级操作去掉,详细地比较一下


列表构造把除了数组操作以外的其他方法按在地上磨擦,数组操作把列表构造按在地上磨擦。值得注意的是,for循环+iat的组合比pandas提供的最快遍历方法apply40%左右,也就是说就算不懂apply的用法,只要把loc/iloc改成at/iat,依然可以有明显的提速。另外,DataFrame的栏位很多的时候,apply_limit方法其实会比对对整个数据框apply快很多(因为不用每次读取整个数据框),只是示范数据的栏位不多所以在这里显现不出差异。

pandas的数组操作和numpy的数组操作单独对比:


列表构造的运行速度是毫秒级的,数组操作是微秒级,np数组操作是pd数组操作的两倍。

作图代码

x = ['loop+iloc','loop+iat','iterrows','apply','apply_limit','list_build','pd_array','np_array']
y_sum = [2.06*1000 , 56.9, 358, 93, 90.3, 1.15, 273/1000, 130/1000] # 單位: ms
y_dot = [2.04*1000 , 58.4, 342, 89.1, 90.7, 1.10, 263/1000, 131/1000] # 單位: ms

res = pd.DataFrame({'method': x*2, 'time': y_sum + y_dot, 'operation': ['sum']*len(x) + ['dot']*len(x)})

import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
sns.set_style('whitegrid')

plt.figure(figsize=(10, 6))
with sns.plotting_context("notebook", font_scale=1.5):
    ax = sns.barplot(x='method', y='time', hue='operation', data=res)
    plt.title('Time usage of different iterating ways')
    plt.ylabel('time usage (ms)')

plt.figure(figsize=(10, 6))
with sns.plotting_context("notebook", font_scale=1.5):
    ax = sns.barplot(x='method', y='time', hue='operation', data=res[~res.method.isin(['loop+iloc','iterrows'])])
    plt.title('Time usage of different iterating ways - detailed')
    plt.ylabel('time usage (ms)')

res_2 = res[res.method.isin(['list_build','pd_array','np_array'])].copy()
res_2['time'] = res_2['time'].values * 1000 

plt.figure(figsize=(10, 6))
with sns.plotting_context("notebook", font_scale=1.5):
    ax = sns.barplot(x='method', y='time', hue='operation', data=res_2)
    plt.title('Time usage of different iterating ways - pd_series vs np_array')
    plt.ylabel('time usage (µs)')
结论
优先使用numpy数组操作!不能数组操作的时候用列表构造!

能用at/iat就不用loc/iloc,能用apply就不用迭代,能用数组操作就不用其他方法。

运行速度:np数组操作 > pd数组操作 >>> np列表构造 = pd列表构造 >>> for循环+at > pd(分片).apply > pd.apply >>> pd.iterrows > for循环+iloc

关于at/iat和loc/iloc的速度比较,请见参考资料[4]

该处使用的url网络请求的数据。


总结

提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值