优化Pandas代码执行速度入门指南
Python部落(python.freelycode.com)组织翻译,禁止转载,欢迎转发。
如果你用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。”
编者勘误:有读者指出“premature optimization” 这句引用的原作者并不是xkcd,而是Donald Knuth。我对这个遗漏道歉,恳请那位读者原谅。
译者:woody
- 深圳西站优化配套设施 确保春运交通顺畅
- 你写的代码,是别人的噩梦吗?
- 容错纠错优化发展软环境
- 正则表达式太慢?这里有一个提速100倍的方案(附代码)
- 银行官网公告将歇业倒闭?实为被人篡改网页源代码
- 从重构到吐血 - 我是如何删掉 6 万行代码并且不删减原有功能的
- 我市人才引进系统将优化升级
- 信用代码营业执照需换新
- 襄阳公安24条措施优化发展环境
- 禁止网络语言暴力,“reword”代码保护着年轻人