优化Pandas代码执行速度入门及循环加快

原创 2019-09-18 22:33  阅读 31 次 评论 0 条

雷锋网 AI 开发者按,如果你使用 python 和 pandas 进行数据分析,那么不久你就会第一次使用循环了。然而,即使是对小型数据集,使用标准循环也很费时,你很快就会意识到大型数据帧可能需要很长的时间。当我第一次等了半个多小时来执行代码时,我找到了接下来想与你共享的替代方案。

标准循环

数据帧是具有行和列的 pandas 对象。如果使用循环,则将遍历整个对象。python 不能用任何内置函数,而且速度非常慢。在我们的示例中,我们得到了一个具有 65 列和 1140 行的数据帧,它包含 2016-2019 赛季的足球比赛结果。我们要创建一个新的列来指示某个特定的队是否打过平局。我们可以这样开始:

def soc_loop(leaguedf,TEAM):
    leaguedf['Draws'] = 99999
    for row in range(0, len(leaguedf)):
        if ((leaguedf['HomeTeam'].iloc[row] == TEAM) & (leaguedf['FTR'].iloc[row] == 'D')) | \
            ((leaguedf['AwayTeam'].iloc[row] == TEAM) & (leaguedf['FTR'].iloc[row] == 'D')):
            leaguedf['Draws'].iloc[row] = 'Draw'
        elif ((leaguedf['HomeTeam'].iloc[row] == TEAM) & (leaguedf['FTR'].iloc[row] != 'D')) | \
            ((leaguedf['AwayTeam'].iloc[row] == TEAM) & (leaguedf['FTR'].iloc[row] != 'D')):
            leaguedf['Draws'].iloc[row] = 'No_Draw'
        else:
            leaguedf['Draws'].iloc[row] = 'No_Game'

因为我们的数据框架中包含了英超的每一场比赛,所以我们必须检查我们感兴趣的球队(阿森纳)是否参加过比赛,是否适用,他们是主队还是客队。如你所见,这个循环非常慢,需要 207 秒才能执行。让我们看看如何提高效率。

pandas 内置函数:iterrow()——快 321 倍

在第一个示例中,我们循环访问了整个数据帧。iterrows()为每行返回一个序列,因此它以一对索引的形式在数据帧上迭代,而感兴趣的列以序列的形式迭代。这使得它比标准循环更快:

def soc_iter(TEAM,home,away,ftr):
    #team, row['HomeTeam'], row['AwayTeam'], row['FTR']
    if [((home == TEAM) & (ftr == 'D')) | ((away == TEAM) & (ftr == 'D'))]:
        result = 'Draw'
    elif [((home == TEAM) & (ftr != 'D')) | ((away == TEAM) & (ftr != 'D'))]:
        result = 'No_Draw'
    else:
        result = 'No_Game'
    return result
 
draw_series=[]
for index,row in df.iterrows():
    draw_series.append(soc_iter('Arsenal',row['HomeTeam'],row['FTR']))
df['draw']=draw_series

代码运行需要 68 毫秒,比标准循环快 321 倍。但是,许多人建议不要使用它,因为仍然有更快的方法,并且 iterrows() 不保留跨行的数据类型。这意味着,如果在数据帧上使用 iterrow(),则可以更改数据类型,这会导致很多问题。要保留数据类型,还可以使用 itertuples()。我们不会在这里详细讨论,因为我们要关注效率。

for row in df.itertuples(index=True, name='Pandas'):
    draw_series.append(soc_iter('Arsenal',getattr(row, 'HomeTeam'),getattr(row, 'FTR')))

apply()方法——快 811 倍

apply 本身并不快,但与数据帧结合使用时具有优势。这取决于应用表达式的内容。如果可以在 Cython 空间中执行,则速度会更快(在这里就是这种情况)。

我们可以将 apply 与 Lambda 函数一起使用。我们要做的就是指定轴。在这种情况下,我们必须使用 axis=1,因为我们要执行一个列操作:

数据处理必看:如何让你的 pandas 循环加快 71803 倍

此代码甚至比以前的方法更快,只需要 27 毫秒就能完成。

pandas 矢量化——快 9280 倍

现在我们可以讨论一个新话题了。我们利用矢量化的优点来创建真正快速的代码。重点是避免像前面的例子 [1] 中那样的 Python 级循环,并使用优化的 C 代码,这个代码使用内存的效率更高。我们只需要稍微修改函数:

def soc_iter(TEAM,home,away,ftr):
    df['Draws'] = 'No_Game'
    df.loc[((home == TEAM) & (ftr == 'D')) | ((away == TEAM) & (ftr == 'D')), 'Draws'] = 'Draw'
    df.loc[((home == TEAM) & (ftr != 'D')) | ((away == TEAM) & (ftr != 'D')), 'Draws'] = 'No_Draw'

现在我们可以用 pandas series 作为输入创建新列:

在这种情况下,我们甚至不需要循环。我们要做的就是调整函数的内容。现在我们可以直接将 pandas series 传递给我们的函数,这会导致巨大的速度增益。

Numpy 矢量化——速度快 71803 倍

在前面的示例中,我们将 pandas series 传递给了函数。通过添加.values,我们收到一个 Numpy 数组:

数据处理必看:如何让你的 pandas 循环加快 71803 倍

Numpy 数组非常快,我们的代码运行时间为 0305 毫秒,比开始使用的标准循环快 71803 倍。

结论

如果您使用 python、pandas 和 Numpy 进行数据分析,那么代码总会有一些改进空间。我们比较了五种不同的方法,在计算的基础上增加了一个新的列到我们的数据框架中。我们注意到在速度方面存在巨大差异:

数据处理必看:如何让你的 pandas 循环加快 71803 倍

如果你从这篇文章中选择两条规则,我会很高兴:

如果确定需要使用循环,则应始终选择 apply 方法

否则,矢量化总是更好的,因为它更快

———————————————————————————————————————

如果你用Python做过一些数据分析相关的项目,那么很有可能你已经接触过Pandas,由Wrs McKinney编写的超赞的数据分析库。通过向Python提供数据框(dataframe)分析功能,Pandas将Python推升到和一些成熟的数据分析工具如R和SAS相近的地位。

不幸的是,一开始Pandas就获得了运行特别慢的名声。必须得承认,你的Pandas代码不太可能达到例如完全由C源码优化的代码的执行速度。好消息是对大多数的应用场景来说,经过足够优化的Pandas代码已经足够快速;Pandas在速度上的缺陷,则由其功能的强大和对用户的友好所弥补。

本文中,我们将按由慢到快的顺序回顾将函数运用到Pandas数据框的几种方法的效率:

  • 1、直接使用索引直接遍历数据框
  • 2、使用iterrows进行循环
  • 3、使用apply循环
  • 4、Pandas序列的矢量化
  • 5、NumPy链表的矢量化

我们将使用Haversine(或者Great Circle)距离公式作为测试函数。该函数取两点的经纬度作为参数,考虑地球表面的曲率,计算两点之间的直线距离。函数看起来大概是下面这个样子:

我们使用了包含纽约所有旅店真实经纬度的数据集来测试该函数,数据来源于 Expedia’s developer site。我们将会计算每个旅店和一组样本坐标(刚好属于纽约的一个很有意思的名为Brooklyn Superhero Supply Store的小商店)的距离。

直接通过索引循环

首先,对Pandas的数据结构基础进行快速回顾。Pandas的基础结构可以分为两种:数据框和序列。数据框是拥有轴标签的二维链表,换言之数据框是拥有标签的行和列组成的矩阵 - 列标签位列名,行标签为索引。Pandas中的行和列是Pandas序列 - 拥有轴标签的一维链表。

基本上我接触过的所有Pandas初学者(当然也包括你们),在某个时刻都会尝试通过遍历将某个固定函数循环应用到数据框的每一行。这种方法的优势在于保持了和Python可迭代对象交互的一致性;例如,遍历列表或者元组的方法。相反,该种方法的缺陷在于,在Pandas中直接使用循环的速度是最慢的。和后面我们提到的方法相比,在Pandas中直接进行循环根本没有使用到任何内置的优化,相比之下显得效率极低,而且通常可读性还更差。

一种常被用到的方法大概是这么写的:为了对上述函数的执行时间有一个直观的感觉,我们使用%timeit命令。%timeit 是Jupyter notebooks的一个魔法命令。(使用单个 % 开头的魔法命令作用于单行,而 %% 开头的命令则作用于整个Jupyter格)。%timeit 将会重复执行函数多次,并打印其接收到的平均运行时间及其标准差。当然,运行函数的系统不同,%timeit 接收的运行时间也不会完全相同。但是,它提供了一个在相同系统相同数据集上比较不同函数运行时间的基准测试工具。


该命令得到以下结果:


直接进行遍历的函数每次运行需要645毫秒,标准差为31毫秒。看上去还挺快,但考虑到该函数仅仅作用于1600行,这个速度其实挺慢了。还是接着看看我们如何提升这个情况吧。

使用iterrows方法进行循环

如果必须使用循环来遍历所有行的话,还有一种更好的办法,那就是使用iterrows方法。iterrows是在数据框中的行进行迭代的一个生成器,它返回每行的索引及一个包含行本身的对象。iterrows 针对Pandas的数据框进行了优化,即便和我们后面提到的几种标准函数相比效率相对较差,但和上面直接进行循环相比已经有了显著提升。同样的功能,iterrows 的速度差不多为直接在行上进行循环的4倍。

使用apply方法更好得进行循环

比iterrows 更好的操作是使用apply 方法,它实现了将函数应用于数据框的特定轴(行、列皆可)。尽管apply 使用的也是在行之间循环的思路,但由于利用了类似Cython的迭代器的一系列全局优化,其效率要比iterrows高很多。

可以使用匿名的lambda函数将Haversine函数应用于每一行,该方法将每行的特定区域作为函数的输入参数。lambda函数的末尾包含axis参数,用来告知Pandas将函数运用于行(axis = 1)或者列(axis = 0)。

使用apply 代替iterrows节省了近一半的函数运行时间。

为了更进一步搞清楚函数执行过程中的时间开销,我们可以使用line profiler tool(Jupyter中的 %lprun魔法命令)。


结果如下:


上述结果中,我们可以得到非常多有用的信息。例如,函数中执行三角函数的运算占用了接近一半的运行时间。因此,如果我们想要对函数的某个部分进行优化,就可以从这里入手了。目前,更值得注意的是每一行代码都被执行了1631次——这也是apply方法在每一行迭代的结果。如果我们可以将重复执行的总量削减下来,总的运行时间也会随之而减少。这也正是接下来谈到的矢量化提升效率的地方。

Pandas series 的矢量化

为了搞清楚如何减少函数迭代执行的总量,我们需要回顾一下Pandas、DataFrame、series的基础单元,以上三种数据结构均基于链表。基础单元的固有结构直接被以整个链表作为参数的函数所调用,而不用按顺序执行每个值(被称为标量)。矢量化即在整个链表上进行操作的过程。

Pandas包括了非常丰富的矢量化函数库:从数学聚合运算到字符串函数(更多可用函数请查看Pandas文档)。内置的函数针对Pandas series和DataFrame进行了特殊优化。结果就是,使用Pandas的矢量化函数几乎都比完成类似目标自定义编写的循环更胜一筹。

截止到目前,我们做的仅仅是吧标量传给Haversine函数。然而,所有Haversine函数中用到的函数也都可以在链表上进行操作。这就使得对距离函数的矢量化变得非常简单:不同于以上的直接将经纬度标量传递给函数,我们直接把整个series(列)作为参数传递。这样做Pandas将充分利用矢量化函数可用的优化,尤其是同时对整个链表进行所有计算。

使用矢量化函数,我们的效率相对apply方法提升了50倍,对iterrows方法提升了100倍 —— 仅仅是修改了输入类型。

接下来看看函数是怎样执行的:可以看到,apply 方法执行了1631次函数,而矢量化版本仅仅执行了一次,因为它是同时作用于整个序列的。这也正是矢量化可以节省如此多的时间的原因。

Numpy arrays的矢量化

到这里,其实已经满足我们的日常需求了;Pandas series的矢量化已经实现了日常计算所需要的绝大部分优化。然而,如果对速度的优先级要求很高,我们可以调用NumPy库的形式进一步优化。

NumPy,自我描述为“Python科学计算的基础包”,使用预编译的C代码在底层进行优化。和Pandas类似,NumPy也是在链表对象上进行操作(被称为ndarrays);不同的是,它避免了Pandas series操作过程中的很多开销,例如索引、数据类型等等。因此,NumPy arrays的操作要比Pandas series快得多。

当Pandas series提供的额外功能不是必须的时候,可以使用NumPy arrays代替Pandas series。例如,我们Haversion函数的矢量化实现实际上并没有用到经纬度series的索引,因此没有这些索引也不会导致函数中断。相反,如果我们的操作中涉及类似DataFrame 拼接,就需要根据索引引用值,这种情况下就只有使用Pandas对象了。

使用values 方法可以很容易将经纬度链表从Pandas series转换为NumPy arrays。就像处理Pandas series类似,直接把NumPy array传给函数将使Pandas在整个矢量应用函数。

在NumPy array上运行速度又提升了四倍。总而言之,我们把运行时间从一开始使用循环的接近半秒钟,通过在NumPy上使用矢量化方法减少到三分之一毫秒!

总结

下面的计分板总结了所有结果。尽管NumPy arrays的矢量化运行速度最快,其边际提升相比Pandas series的矢量化还是略显平缓,相比最快的循环Pandas series矢量化速度提升达到56倍。


对Pandas代码进行优化的一些总结如下:

  • 1、避免使用循环;循环的执行速度慢,有时候还很不必要;
  • 2、不得不使用循环时,使用apply 方法,而不是循环调用函数;
  • 3、矢量化比使用标量操作更好。Pandas中大多数的操作都可以被矢量化。
  • 4、NumPy arrays上的矢量化操作要比Pandas series更高效。

当然,以上几点并不是对Pandas进行优化的完整列表。例如,更激进的用户可能会考虑,使用重写Cython中的函数,或者尝试优化函数的各个部分。但这些就已经超出本文讨论的范畴了。

最重要的是,在开启一次庞大的优化冒险旅程之前,先确保你打算优化的函数确实是之后会长期使用的函数。引用xkcd不朽的名言:“过早优化是万恶之源Premature optimization is the root of all evil。”

本文地址:http://51blog.com/?p=3362
关注我们:请关注一下我们的微信公众号:扫描二维码广东高校数据家园_51博客的公众号,公众号:数博联盟
版权声明:本文为原创文章,版权归 jnussl 所有,欢迎分享本文,转载请保留出处!

发表评论


表情