for是所有编程语言的基本语法。初学者为了快速实现功能,比较懒惰。但在计算时间性能上可能不是特别好的选择。这次东哥介绍几种常见的提速方法,一个比一个快,只有了解pandas的本性,才能知道如何提速。下面是一个例子,数据的获取方法在文末。>>>importpandasaspd#导入数据集>>>df=pd.read_csv('demand_profile.csv')>>>df.head()date_timeenergy_kwh01/1/130:000.58611/1/131:000.58021/1/132:000.57231/1/133:000.59641/1/134:000.592基于以上数据,我们现在要增加一个新的特征,但是这个新特征是根据一些时间条件产生的,并且根据时间的长短(小时)而变化,如如下:所以,如果不知道怎么提速,那么正常的第一个想到的可能是用apply方法写一个函数,把时间条件的逻辑代码写在函数里面。defapply_tariff(kwh,hour):"""计算每小时电费"""if0<=hour<7:rate=12elif7<=hour<17:rate=20elif17<=hour<24:rate=28else:raiseValueError(f'Invalidhour:{hour}')returnrate*kwh然后用for循环遍历df,按照apply函数的逻辑添加新的特征,如下:>>>#不同意这个操作>>>@timeit(repeat=3,number=100)...defapply_tariff_loop(df):..."""使用for循环计算energycost并将其添加到列表中"""...energy_cost_list=[]...foriinrange(len(df)):...#获取耗电量和时间(小时)...energy_used=df.iloc[i]['energy_kwh']...hour=df.iloc[i]['date_time'].hour...energy_cost=apply_tariff(energy_used,hour)...energy_cost_list.append(energy_cost)...df['cost_cents']=energy_cost_list...>>>apply_tariff_loop(df)Bestof3trialswith100functioncallspertrial:Function`apply_tariff_loop`raninaverageof3.152seconds对于那些写ForPythonic人们,这个设计看起来很自然。但是,这种循环会严重影响效率。这有几个原因:首先,它需要初始化一个将记录输出的列表。其次,它使用不透明对象range(0,len(df))进行循环,然后在应用apply_tariff()之后,它必须将结果附加到用于创建新DataFrame列的列表中。此外,df.iloc[i]['date_time']用于执行所谓的链式索引,这通常会导致意外结果。这种方法的最大问题是计算的时间成本。对于8760行数据,此循环耗时3秒。接下来我们来看看优化后的提速方案。使用iterrows循环可以通过pandas引入iterrows方法使其更加高效。这些是一次生成一行的生成器方法,类似于scrapy中使用的yield用法。.itertuples为每一行生成一个命名元组,并将该行的索引值作为元组的第一个元素。名称元组是Python集合模块中的一种数据结构,其行为类似于Python元组,但具有可通过属性查找访问的字段。.iterrows为DataFrame中的每一行生成类似(index,series)的元组。在此示例中使用.iterrows,让我们看看它如何与iterrows一起使用。>>>@timeit(repeat=3,number=100)...defapply_tariff_iterrows(df):...energy_cost_list=[]...forindex,rowindf.iterrows():...#获取耗电量和时间(hour)...energy_used=row['energy_kwh']...hour=row['date_time'].hour...#addcostlist...energy_cost=apply_tariff(energy_used,hour)...energy_cost_list。append(energy_cost)...df['cost_cents']=energy_cost_list...>>>apply_tariff_iterrows(df)Bestof3trialswith100functioncallspertrial:Function`apply_tariff_iterrows`raninaverageof0.713seconds。这种语法更明确,行值引用中的混淆更少,因此可读性更强。在时间成本方面:快了近5倍!然而,还有更多的改进空间,理想情况下,这可以通过pandas内置的更快的方法来完成。pandas的apply方法我们可以通过使用.apply方法而不是.iterrows来进一步改进这个操作。pandas的.apply方法接受可调用函数并沿DataFrame的轴(所有行或所有列)应用。在以下代码中,lambda函数将两列数据传递给apply_tariff():>>>@timeit(repeat=3,number=100)...defapply_tariff_withapply(df):...df['cost_cents']=df。应用(...lambdarow:apply_tariff(...kwh=row['energy_kwh'],...hour=row['date_time'].hour),...axis=1)...>>>apply_tariff_withapply(df)Bestof3trialswith100functioncallspertrial:Function`apply_tariff_withapply`raninaverageof0.272seconds.apply语法优势明显,行数更少,代码可读性更高。在这种情况下,所花费的时间大约是iterrows方法的一半。但是,这并不是“非常快”。一个原因是apply()将在内部尝试循环Cython迭代器。但是在这种情况下,传递的lambda不是Cython中可以处理的东西,因此在Python中调用并没有那么快。如果我们使用apply()方法获取10年的每小时数据,那么大约需要15分钟来处理。如果这个计算只是大规模计算的一小部分,那么它应该真的会加速。这就是向量化操作派上用场的地方。矢量化操作:使用.isin选择数据什么是矢量化操作?如果不是基于某种条件,您可以在一行代码中将所有用电量数据应用于该价格:df['energy_kwh']*28,类似这样。好吧,这个特定的操作是矢量化操作的一个例子,它是在pandas中完成它的最快方法。但是如何将条件计算应用为pandas中的矢量化运算呢?一个技巧是:根据您的标准选择DataFrame并对其进行分组,然后对每个选定的组应用矢量化操作。在下面的代码中,我们将看到如何使用pandas.isin()方法来选择行,然后在向量化操作中实现添加新功能。在这样做之前,如果将date_time列设置为DataFrame的索引会更方便:3,number=100)defapply_tariff_isin(df):#定义小时范围布尔数组peak_hours=df.index.hour.isin(range(17,24))shoulder_hours=df.index.hour.isin(range(7,17))off_peak_hours=df.index.hour.isin(range(0,7))#使用上面apply_traffic函数中的定义df.loc[peak_hours,'cost_cents']=df.loc[peak_hours,'energy_kwh']*28df.loc[shoulder_hours,'cost_cents']=df.loc[shoulder_hours,'energy_kwh']*20df.loc[off_peak_hours,'cost_cents']=df.loc[off_peak_hours,'energy_kwh']*12让我们看看结果。>>>apply_tariff_isin(df)Bestof3trialswith100functioncallspertrial:Function`apply_tariff_isin`raninaverageof0.010seconds。提示,上面的.isin()方法返回一个布尔值数组,如下:[False,False,False,...,True,True,True]布尔值表示DataFrame索引的日期时间是否在指定的小时范围内.然后将这些布尔数组传递给DataFrame的.loc将获得与这些时间匹配的DataFrame的一部分。然后将切片乘以适当的速率,这是一种快速矢量化操作。上面的方法完全替代了我们原来的自定义函数apply_tariff(),代码大大减少,速度起飞。运行时间比Pythonicfor循环快315倍,比iterrows快71倍,比apply快27倍!能不能快点?太刺激了,我们继续加速。在上面的apply_tariff_isin中,我们通过三次调用df.loc和df.index.hour.isin进行了一些手动调整。如果我们有更好的时间框架,您可能会说此解决方案不可扩展。但在这种情况下,我们可以使用pandas的pd.cut()函数来自动完成切割:@timeit(repeat=3,number=100)defapply_tariff_cut(df):cents_per_kwh=pd.cut(x=df.index。hour,bins=[0,7,17??,24],include_lowest=True,labels=[12,20,28]).astype(int)df['cost_cents']=cents_per_kwh*df['energy_kwh']以上代码pd.cut()将根据bin列表应用分组。其中include_lowest参数指示第一个间隔是否应为左侧间隔。这是一种完全矢量化的方法,就时间而言是最快的:>>>apply_tariff_cut(df)Bestof3trialswith100functioncallspertrial:Function`apply_tariff_cut`raninaverageof0.003seconds。至此,使用pandas的处理时间基本达到了上限!处理完整的10年每小时数据集只需不到一秒钟。然而,最后一个选择是使用NumPy,它可以更快!在使用pandas继续使用Numpy加速时不应忘记的一件事是Pandas的Series和DataFrames是在NumPy库之上设计的。此外,pandas可与NumPy数组和操作无缝协作。接下来,我们使用NumPy的digitize()函数更进一步。它与上面的pandas的cut()类似,因为数据将被分箱,但这次它将由一个索引数组表示,指示每小时属于哪个分箱。然后将这些指数应用于价格数组:@timeit(repeat=3,number=100)defapply_tariff_digitize(df):prices=np.array([12,20,28])bins=np.digitize(df.index.hour.values,bins=[7,17,24])df['cost_cents']=prices[bins]*df['energy_kwh'].values和cut函数一样,这个语法非常简洁易于阅读。>>>apply_tariff_digitize(df)每次试验100次函数调用的3次试验中的最佳试验:函数`apply_tariff_digitize`的平均运行时间为0.002秒。0.002秒!虽然还有性能提升,但是已经很边缘了。
