再见 for 循环!pandas 提速 315 倍!

来源:Python数据科学

作者:东哥起飞

上一篇分享了一个从时间处理上的加速方法「使用 Datetime 提速 50 倍运行速度!」,本篇分享一个更常用的加速骚操作。

for是所有编程语言的基础语法,初学者为了快速实现功能,依懒性较强。但如果从运算时间性能上考虑可能不是特别好的选择。

本次东哥介绍几个常见的提速方法,一个比一个快,了解pandas本质,才能知道如何提速。

下面是一个例子,数据获取方式见文末。

1>>> import pandas as pd 2# 导入数据集 3>>> df = pd.read_csv('demand_profile.csv') 4>>> df.head() 5 date_time energy_kwh 60 1/1/13 0:00 0.586 71 1/1/13 1:00 0.580 82 1/1/13 2:00 0.572 93 1/1/13 3:00 0.596 104 1/1/13 4:00 0.592

基于上面的数据,我们现在要增加一个新的特征,但这个新的特征是基于一些时间条件生成的,根据时长(小时)而变化,如下:

因此,如果你不知道如何提速,那正常第一想法可能就是用apply方法写一个函数,函数里面写好时间条件的逻辑代码。

1def apply_tariff(kwh, hour): 2 """计算每个小时的电费""" 3 if 0 <= hour < 7: 4 rate = 12 5 elif 7 <= hour < 17: 6 rate = 20 7 elif 17 <= hour < 24: 8 rate = 28 9 else: 10 raise ValueError(f'Invalid hour: {hour}') 11 return rate * kwh

然后使用for循环来遍历df,根据apply函数逻辑添加新的特征,如下:

1>>> # 不赞同这种操作 2>>> @timeit(repeat=3, number=100) 3... def apply_tariff_loop(df): 4... """用for循环计算enery cost,并添加到列表""" 5... energy_cost_list = [] 6... for i in range(len(df)): 7... # 获取用电量和时间(小时) 8... energy_used = df.iloc[i]['energy_kwh'] 9... hour = df.iloc[i]['date_time'].hour 10... energy_cost = apply_tariff(energy_used, hour) 11... energy_cost_list.append(energy_cost) 12... df['cost_cents'] = energy_cost_list 13... 14>>> apply_tariff_loop(df) 15Best of 3 trials with 100 function calls per trial: 16Function `apply_tariff_loop` ran in average of 3.152 seconds. 17

对于那些写Pythonic风格的人来说,这个设计看起来很自然。然而,这个循环将会严重影响效率。原因有几个:

首先,它需要初始化一个将记录输出的列表。

其次,它使用不透明对象范围(0,len(df))循环,然后再应用apply_tariff()之后,它必须将结果附加到用于创建新DataFrame列的列表中。另外,还使用df.iloc [i]['date_time']执行所谓的链式索引,这通常会导致意外的结果。

这种方法的最大问题是计算的时间成本。对于8760行数据,此循环花费了3秒钟。

接下来,一起看下优化的提速方案。

一、使用 iterrows循环

第一种可以通过pandas引入iterrows方法让效率更高。这些都是一次产生一行的生成器方法,类似scrapy中使用的yield用法。

.itertuples为每一行产生一个namedtuple,并且行的索引值作为元组的第一个元素。nametuplePythoncollections模块中的一种数据结构,其行为类似于Python元组,但具有可通过属性查找访问的字段。

.iterrowsDataFrame中的每一行产生(index,series)这样的元组。

在这个例子中使用.iterrows,我们看看这使用iterrows后效果如何。

1>>> @timeit(repeat=3, number=100) 2... def apply_tariff_iterrows(df): 3... energy_cost_list = [] 4... for index, row in df.iterrows(): 5... # 获取用电量和时间(小时) 6... energy_used = row['energy_kwh'] 7... hour = row['date_time'].hour 8... # 添加cost列表 9... energy_cost = apply_tariff(energy_used, hour) 10... energy_cost_list.append(energy_cost) 11... df['cost_cents'] = energy_cost_list 12... 13>>> apply_tariff_iterrows(df) 14Best of 3 trials with 100 function calls per trial: 15Function `apply_tariff_iterrows` ran in average of 0.713 seconds. 16

这样的语法更明确,并且行值引用中的混乱更少,因此它更具可读性。

时间成本方面:快了近5倍!

但是,还有更多的改进空间,理想情况是可以用pandas内置更快的方法完成。

二、pandas的apply方法

我们可以使用.apply方法而不是.iterrows进一步改进此操作。pandas.apply方法接受函数callables并沿DataFrame的轴(所有行或所有列)应用。下面代码中,lambda函数将两列数据传递给apply_tariff()

1>>> @timeit(repeat=3, number=100) 2... def apply_tariff_withapply(df): 3... df['cost_cents'] = df.apply( 4... lambda row: apply_tariff( 5... kwh=row['energy_kwh'], 6... hour=row['date_time'].hour), 7... axis=1) 8... 9>>> apply_tariff_withapply(df) 10Best of 3 trials with 100 function calls per trial: 11Function `apply_tariff_withapply` ran in average of 0.272 seconds.

apply的语法优点很明显,行数少,代码可读性高。在这种情况下,所花费的时间大约是iterrows方法的一半。

但是,这还不是“非常快”。一个原因是apply()将在内部尝试循环遍历Cython迭代器。但是在这种情况下,传递的lambda不是可以在Cython中处理的东西,因此它在Python中调用并不是那么快。

如果我们使用apply()方法获取10年的小时数据,那么将需要大约15分钟的处理时间。如果这个计算只是大规模计算的一小部分,那么真的应该提速了。这也就是矢量化操作派上用场的地方。

三、矢量化操作:使用.isin选择数据

什么是矢量化操作?

如果你不基于一些条件,而是可以在一行代码中将所有电力消耗数据应用于该价格:df ['energy_kwh'] * 28,类似这种。那么这个特定的操作就是矢量化操作的一个例子,它是在pandas中执行的最快方法。

但是如何将条件计算应用为pandas中的矢量化运算?

一个技巧是:根据你的条件,选择和分组**DataFrame****,然后对每个选定的组应用矢量化操作。**

在下面代码中,我们将看到如何使用pandas.isin()方法选择行,然后在矢量化操作中实现新特征的添加。在执行此操作之前,如果将date_time列设置为DataFrame的索引,会更方便:

1# 将date_time列设置为DataFrame的索引 2df.set_index('date_time', inplace=True) 3 4@timeit(repeat=3, number=100) 5def apply_tariff_isin(df): 6 # 定义小时范围Boolean数组 7 peak_hours = df.index.hour.isin(range(17, 24)) 8 shoulder_hours = df.index.hour.isin(range(7, 17)) 9 off_peak_hours = df.index.hour.isin(range(0, 7)) 10 11 # 使用上面apply_traffic函数中的定义 12 df.loc[peak_hours, 'cost_cents'] = df.loc[peak_hours, 'energy_kwh'] * 28 13 df.loc[shoulder_hours,'cost_cents'] = df.loc[shoulder_hours, 'energy_kwh'] * 20 14 df.loc[off_peak_hours,'cost_cents'] = df.loc[off_peak_hours, 'energy_kwh'] * 12

我们来看一下结果如何。

1>>> apply_tariff_isin(df) 2Best of 3 trials with 100 function calls per trial: 3Function `apply_tariff_isin` ran in average of 0.010 seconds.

提示,上面.isin()方法返回的是一个布尔值数组,如下:

[False, False, False, ..., True, True, True]

布尔值标识了DataFrame索引datetimes是否落在了指定的小时范围内。然后把这些布尔数组传递给DataFrame.loc,将获得一个与这些小时匹配的DataFrame切片。然后再将切片乘以适当的费率,这就是一种快速的矢量化操作了。

上面的方法完全取代了我们最开始自定义的函数apply_tariff(),代码大大减少,同时速度起飞。

运行时间比Pythonic的for循环快315倍,比iterrows快71倍,比apply快27倍!

四、还能更快?

太刺激了,我们继续加速。

在上面apply_tariff_isin中,我们通过调用df.locdf.index.hour.isin三次来进行一些手动调整。如果我们有更精细的时间范围,你可能会说这个解决方案是不可扩展的。但在这种情况下,我们可以使用pandaspd.cut()函数来自动完成切割:

1@timeit(repeat=3, number=100) 2def apply_tariff_cut(df): 3 cents_per_kwh = pd.cut(x=df.index.hour, 4 bins=[0, 7, 17, 24], 5 include_lowest=True, 6 labels=[12, 20, 28]).astype(int) 7 df['cost_cents'] = cents_per_kwh * df['energy_kwh']

上面代码pd.cut()会根据bin列表应用分组。

其中include_lowest参数表示第一个间隔是否应该是包含左边的。

这是一种完全矢量化的方法,它在时间方面是最快的:

1>>> apply_tariff_cut(df) 2Best of 3 trials with 100 function calls per trial: 3Function `apply_tariff_cut` ran in average of 0.003 seconds.

到目前为止,使用pandas处理的时间上基本快达到极限了!只需要花费不到一秒的时间即可处理完整的10年的小时数据集。

但是,最后一个其它选择,就是使用 NumPy,还可以更快!

五、使用Numpy继续加速

使用pandas时不应忘记的一点是PandasSeriesDataFrames是在NumPy库之上设计的。并且,pandas可以与NumPy阵列和操作无缝衔接。

下面我们使用NumPydigitize()函数更进一步。它类似于上面pandascut(),因为数据将被分箱,但这次它将由一个索引数组表示,这些索引表示每小时所属的bin。然后将这些索引应用于价格数组:

1@timeit(repeat=3, number=100) 2def apply_tariff_digitize(df): 3 prices = np.array([12, 20, 28]) 4 bins = np.digitize(df.index.hour.values, bins=[7, 17, 24]) 5 df['cost_cents'] = prices[bins] * df['energy_kwh'].values

cut函数一样,这种语法非常简洁易读。

1>>> apply_tariff_digitize(df) 2Best of 3 trials with 100 function calls per trial: 3Function `apply_tariff_digitize` ran in average of 0.002 seconds.

0.002秒! 虽然仍有性能提升,但已经很边缘化了。

-------------------********************************** End **********-------------**-----********-**********************************

往期精彩文章推荐:

欢迎各位大佬点击链接加入群聊【helloworld开发者社区】:https://jq.qq.com/?_wv=1027&k=mBlk6nzX进群交流IT技术热点。

本文转自 https://mp.weixin.qq.com/s/M3pwGTqt7bigo06NSy_QDg,如有侵权,请联系删除。

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

Java日期时间API系列31

  时间戳是指格林威治时间1970年01月01日00时00分00秒起至现在的总毫秒数,是所有时间的基础,其他时间可以通过时间戳转换得到。Java中本来已经有相关获取时间戳的方法,Java8后增加新的类Instant等专用于处理时间戳问题。 1获取时间戳的方法和性能对比1.1获取时间戳方法Java8以前

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )

​一篇文章总结一下Python库中关于时间的常见操作

前言本次来总结一下关于Python时间的相关操作,有一个有趣的问题。如果你的业务用不到时间相关的操作,你的业务基本上会一直用不到。但是如果你的业务一旦用到了时间操作,你就会发现,淦,到处都是时间操作。。。所以思来想去,还是总结一下吧,本次会采用类型注解方式。time包importtime时间戳从1970年1月1日00:00:00标准时区诞生到现在