pandasapplylambda_⽤这⼏个⽅法提⾼pandas运⾏速度
前⾔
当⼤家谈到数据分析时,提及最多的语⾔就是Python和SQL。Python之所以适合数据分析,是因为它有很多第三⽅强⼤的库来协
助,pandas
pandas就是其中之⼀。pandas的⽂档中是这样描述的:
“快速,灵活,富有表现⼒的数据结构,旨在使”关系“或”标记“数据的使⽤既简单⼜直观。”
dataframe和series
series,我们对数据的⼀些操作都是基于这两个数据结构的。但在实际的使⽤中,我们知道pandas的两个主要数据结构:dataframe
我们可能很多时候会感觉运⾏⼀些数据结构的操作会异常的慢。⼀个操作慢⼏秒可能看不出来什么,但是⼀整个项⽬中很多个操作加起来会让整个开发⼯作效率变得很低。有的朋友抱怨pandas简直太慢了,其实对于pandas的⼀些操作也是有⼀定技巧的。
pandas是基于numpy库的数组结构上构建的,并且它的很多操作都是(通过numpy或者pandas⾃⾝由Cpython实现并编译成C的扩展模块)在C语⾔中实现的。因此,如果正确使⽤pandas的话,它的运⾏速度应该是⾮常快的。
本篇将要介绍⼏种pandas中常⽤到的⽅法,对于这些⽅法使⽤存在哪些需要注意的问题,以及如何对它们进⾏速度提升。
将datetime数据与时间序列⼀起使⽤的优点
进⾏批量计算的最有效途径
通过HDFStore存储数据节省时间
使⽤Datetime数据节省时间
我们来看⼀个例⼦。
dtypes 的概念。如果没有特殊声明,那么
从运⾏上⾯代码得到的结果来看,好像没有什么问题。但实际上pandas和numpy都有⼀个dtypes
object 的dtype类型,如下⾯代码所⽰:
date_time将会使⽤⼀个 object
object 类型像⼀个⼤的容器,不仅仅可以承载 str,也可以包含那些不能很好地融进⼀个数据类型的任何特征列。⽽如果我们将⽇期作为str 类型就会极⼤的影响效率。
时间戳)。pandas在这⾥因此,对于时间序列的数据⽽⾔,我们需要让上⾯的date_time列格式化为datetime对象数组(pandas称之为时间戳
操作⾮常简单,操作如下:
我们来运⾏⼀下这个df看看转化后的效果是什么样的。
date_time的格式已经⾃动转化了,但这还没完,在这个基础上,我们还是可以继续提⾼运⾏速度的。如何提速呢?为了更好的对⽐,我们 timeit 装饰器来测试⼀下上⾯代码的转化时间。
⾸先通过 timeit
1.61s,看上去挺快,但其实可以更快,我们来看⼀下下⾯的⽅法
ISO 8601 格式的,如
设置了转化的格式format。由于在CSV中的datetimes并不是 ISO 8601
结果只有0.032s,快了将近50倍。原因是:我们设置了转化的格式format
dateutil 包把每个字符串str转化成date⽇期。
果不进⾏设置的话,那么pandas将使⽤ dateutil
相反,如果原始数据datetime已经是 ISO 8601 格式了,那么pandas就可以⽴即使⽤最快速的⽅法来解析⽇期。这也就是为什么提前设置好格式format可以提升这么多。
pandas数据的循环操作
仍然基于上⾯的数据,我们想添加⼀个新的特征,但这个新的特征是基于⼀些时间条件的,根据时长(⼩时)⽽变化,如下:
因此,按照我们正常的做法就是使⽤apply⽅法写⼀个函数,函数⾥⾯写好时间条件的逻辑代码。
然后使⽤for循环来遍历df,根据apply函数逻辑添加新的特征,如下:
对于那些写Pythonic风格的⼈来说,这个设计看起来很⾃然。然⽽,这个循环将会严重影响效率,也是不赞同这么做。原因有⼏个:
⾸先,它需要初始化⼀个将记录输出的列表。
其次,它使⽤不透明对象范围(0,len(df))循环,然后在应⽤apply_tariff()之后,它必须将结果附加到⽤于创建新DataFrame列的列表中。它还使⽤df.iloc [i] ['date_time']执⾏所谓的链式索引,这通常会导致意外的结果。
但这种⽅法的最⼤问题是计算的时间成本
计算的时间成本。对于8760⾏数据,此循环花费了3秒钟。接下来,你将看到⼀些改进的Pandas结构迭代解决⽅案。
使⽤itertuples() 和iterrows() 循环
那么推荐做法是什么样的呢?
iterrows⽅法可以使效率更快。这些都是⼀次产⽣⼀⾏的⽣成器⽅法,类似scrapy中使⽤
实际上可以通过pandas引⼊itertuples
itertuples和iterrows
yield⽤法。
的yield
namedtuple,并且⾏的索引值作为元组的第⼀个元素。nametuple是Python的collections
collections模块中的⼀种.itertuples为每⼀⾏产⽣⼀个namedtuple
数据结构,其⾏为类似于Python元组,但具有可通过属性查访问的字段。
.iterrows为DataFrame中的每⼀⾏产⽣(index,series)这样的元组。
虽然.itertuples往往会更快⼀些,但是在这个例⼦中使⽤.iterrows,我们看看这使⽤iterrows后效果如何。
语法⽅⾯:这样的语法更明确,并且⾏值引⽤中的混乱更少,因此它更具可读性。
语法⽅⾯:
在时间收益⽅⾯:快了近5倍! 但是,还有更多的改进空间。我们仍然在使⽤某种形式的Python for循环,这意味着每个函数调⽤都是在在时间收益⽅⾯:
Python中完成的,理想情况是它可以⽤Pandas内部架构中内置的更快的语⾔完成。
Pandas的 .apply()⽅法
.apply⽅法⽽不是.iterrows进⼀步改进此操作。Pandas的.apply⽅法接受函数(callables)并沿DataFrame的轴(所有⾏或所我们可以使⽤.apply
有列)应⽤它们。在此⽰例中,lambda函数将帮助你将两列数据传递给apply_tariff():
.apply的语法优点很明显,⾏数少,代码可读性⾼。在这种情况下,所花费的时间⼤约是.iterrows⽅法的⼀半。
传递的lambda不是可但是,这还不是“⾮常快”。⼀个原因是.apply()将在内部尝试循环遍历Cython迭代器。
⼀个原因是.apply()将在内部尝试循环遍历Cython迭代器。但是在这种情况下,传递的lambda不是可
以在Cython中处理的东西,因此它在Python中调⽤,因此并不是那么快。
如果你使⽤.apply()获取10年的⼩时数据,那么你将需要⼤约15分钟的处理时间。如果这个计算只是⼤型模型的⼀⼩部分,那么你真的应
⽮量化操作派上⽤场的地⽅。
该加快速度。这也就是⽮量化操作
⽮量化操作:使⽤.isin()选择数据
什么是⽮量化操作?如果你不基于⼀些条件,⽽是可以在⼀⾏代码中将所有电⼒消耗数据应⽤于该价格(df ['energy_kwh'] * 28),类似这什么是⽮量化操作?
种。这个特定的操作就是⽮量化操作的⼀个例⼦,它是在Pandas中执⾏的最快⽅法。
根据你的条件选择和分组DataFrame,然后对每个选定的组应⽤但是如何将条件计算应⽤为Pandas中的⽮量化运算?
但是如何将条件计算应⽤为Pandas中的⽮量化运算?⼀个技巧是根据你的条件选择和分组DataFrame,然后对每个选定的组应⽤
⽮量化操作。 在下⼀个⽰例中,你将看到如何使⽤Pandas的.isin()⽅法选择⾏,然后在向量化操作中实现上⾯新特征的添加。在执⾏此操⽮量化操作。
作之前,如果将date_time列设置为DataFrame的索引,则会使事情更⽅便:
我们来看⼀下结果如何。
.isin()⽅法返回的是⼀个布尔值数组,如下所⽰:
为了了解刚才代码中发⽣的情况,我们需要知道.isin()⽅法返回的是⼀个布尔值数组
[False, False, False, ..., True, True, True]
这些值标识哪些DataFrame索引(datetimes)落在指定的⼩时范围内。然后,当你将这些布尔数组传递给DataFrame的.loc索引器时,你将lambda编程
这是⼀种快速的⽮量化操作。
获得⼀个仅包含与这些⼩时匹配的⾏的DataFrame切⽚。在那之后,仅仅是将切⽚乘以适当的费率,这是⼀种快速的⽮量化操作
这与我们上⾯的循环操作相⽐如何?⾸先,你可能会注意到不再需要apply_tariff(),因为所有条件逻辑都应⽤于⾏的选择。因此,你必须编写的代码⾏和调⽤的Python代码会⼤⼤减少。
⽐不是Pythonic的循环15倍,⽐.iterrows快71倍,⽐.apply快27倍。
处理时间怎么样?⽐不是Pythonic的循环15倍,⽐.iterrows快71倍,⽐.apply快27倍。
还可以做的更好吗?
在apply_tariff_isin中,我们仍然可以通过调⽤df.loc和df.index.hour.isin三次来进⾏⼀些“⼿动⼯作”。如果我们有更精细的时隙范围,
pd.cut() 函数以编程⽅式执⾏更多操作:你可能会争辩说这个解决⽅案是不可扩展的。幸运的是,在这种情况下,你可以使⽤Pandas的pd.cut()
让我们看看这⾥发⽣了什么。pd.cut() 根据每⼩时所属的bin应⽤⼀组标签(costs)。
注意include_lowest参数表⽰第⼀个间隔是否应该是包含左边的(您希望在组中包含时间= 0)。
完全⽮量化的⽅式来获得我们的预期结果,它在时间⽅⾯是最快的:
这是⼀种完全⽮量化
到⽬前为⽌,时间上基本快达到极限了,只需要花费不到⼀秒的时间来处理完整的10年的⼩时数据集。但是,最后⼀个选项是使⽤NumPy
NumPy 函数来操作每个DataFrame的底层NumPy数组,然后将结果集成回Pandas数据结构中。
使⽤Numpy继续加速
使⽤Pandas时不应忘记的⼀点是Pandas Series和DataFrames是在NumPy库之上设计的。这为你提供了更多的计算灵活性,因为Pandas可以与NumPy阵列和操作⽆缝衔接。
下⾯,我们将使⽤NumPy的 digitize()
digitize() 函数。它类似于Pandas的cut(),因为数据将被分箱,但这次它将由⼀个索引数组表⽰,这些索引表⽰每⼩时所属的bin。然后将这些索引应⽤于价格数组:
与cut函数⼀样,这种语法⾮常简洁易读。但它在速度⽅⾯有何⽐较?让我们来看看:
在这⼀点上,仍然有性能提升,但它本质上变得更加边缘化。使⽤Pandas,它可以帮助维持“层次结构”,如果你愿意,可以像在此处⼀样进⾏批量计算,这些通常排名从最快到最慢(最灵活到最不灵活):
1. 使⽤向量化操作:没有for循环的Pandas⽅法和函数。
1. 使⽤向量化操作:
2. 将.apply⽅法:与可调⽤⽅法⼀起使⽤。
2. 将.apply⽅法:
3. 使⽤.itertuples:从Python的集合模块迭代DataFrame⾏作为namedTuples。
3. 使⽤.itertuples:
4. 使⽤.iterrows:迭代DataFrame⾏作为(index,Series)对。虽然Pandas系列是⼀种灵活的数据结构,但将每⼀⾏构建到⼀个系列中4. 使⽤.iterrows:
然后访问它可能会很昂贵。
5. 使⽤“element-by-element”循环:使⽤df.loc或df.iloc⼀次更新⼀个单元格或⾏。
5. 使⽤“element-by-element”循环: