再見 for 循環(huán)!pandas 提速 315 倍~
本篇分享一個(gè)常用的加速騷操作。
for是所有編程語言的基礎(chǔ)語法,初學(xué)者為了快速實(shí)現(xiàn)功能,依懶性較強(qiáng)。但如果從運(yùn)算時(shí)間性能上考慮可能不是特別好的選擇。pandas本質(zhì),才能知道如何提速。>>> import pandas as pd
# 導(dǎo)入數(shù)據(jù)集
>>> df = pd.read_csv('demand_profile.csv')
>>> df.head()
date_time energy_kwh
0 1/1/13 0:00 0.586
1 1/1/13 1:00 0.580
2 1/1/13 2:00 0.572
3 1/1/13 3:00 0.596
4 1/1/13 4:00 0.592

因此,如果你不知道如何提速,那正常第一想法可能就是用apply方法寫一個(gè)函數(shù),函數(shù)里面寫好時(shí)間條件的邏輯代碼。
def apply_tariff(kwh, hour):
"""計(jì)算每個(gè)小時(shí)的電費(fèi)"""
if 0 <= hour < 7:
rate = 12
elif 7 <= hour < 17:
rate = 20
elif 17 <= hour < 24:
rate = 28
else:
raise ValueError(f'Invalid hour: {hour}')
return rate * kwh
for循環(huán)來遍歷df,根據(jù)apply函數(shù)邏輯添加新的特征,如下:>>> # 不贊同這種操作
>>> @timeit(repeat=3, number=100)
... def apply_tariff_loop(df):
... """用for循環(huán)計(jì)算enery cost,并添加到列表"""
... energy_cost_list = []
... for i in range(len(df)):
... # 獲取用電量和時(shí)間(小時(shí))
... 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)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_loop` ran in average of 3.152 seconds.
Pythonic風(fēng)格的人來說,這個(gè)設(shè)計(jì)看起來很自然。然而,這個(gè)循環(huán)將會(huì)嚴(yán)重影響效率。原因有幾個(gè):(0,len(df))循環(huán),然后再應(yīng)用apply_tariff()之后,它必須將結(jié)果附加到用于創(chuàng)建新DataFrame列的列表中。另外,還使用df.iloc [i]['date_time']執(zhí)行所謂的鏈?zhǔn)剿饕?,這通常會(huì)導(dǎo)致意外的結(jié)果。一、使用 iterrows循環(huán)
pandas引入iterrows方法讓效率更高。這些都是一次產(chǎn)生一行的生成器方法,類似scrapy中使用的yield用法。.itertuples為每一行產(chǎn)生一個(gè)namedtuple,并且行的索引值作為元組的第一個(gè)元素。nametuple是Python的collections模塊中的一種數(shù)據(jù)結(jié)構(gòu),其行為類似于Python元組,但具有可通過屬性查找訪問的字段。.iterrows為DataFrame中的每一行產(chǎn)生(index,series)這樣的元組。.iterrows,我們看看這使用iterrows后效果如何。>>> @timeit(repeat=3, number=100)
... def apply_tariff_iterrows(df):
... energy_cost_list = []
... for index, row in df.iterrows():
... # 獲取用電量和時(shí)間(小時(shí))
... energy_used = row['energy_kwh']
... hour = row['date_time'].hour
... # 添加cost列表
... energy_cost = apply_tariff(energy_used, hour)
... energy_cost_list.append(energy_cost)
... df['cost_cents'] = energy_cost_list
...
>>> apply_tariff_iterrows(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_iterrows` ran in average of 0.713 seconds.
pandas內(nèi)置更快的方法完成。二、pandas的apply方法
.apply方法而不是.iterrows進(jìn)一步改進(jìn)此操作。pandas的.apply方法接受函數(shù)callables并沿DataFrame的軸(所有行或所有列)應(yīng)用。下面代碼中,lambda函數(shù)將兩列數(shù)據(jù)傳遞給apply_tariff():>>> @timeit(repeat=3, number=100)
... def apply_tariff_withapply(df):
... df['cost_cents'] = df.apply(
... lambda row: apply_tariff(
... kwh=row['energy_kwh'],
... hour=row['date_time'].hour),
... axis=1)
...
>>> apply_tariff_withapply(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_withapply` ran in average of 0.272 seconds.
apply的語法優(yōu)點(diǎn)很明顯,行數(shù)少,代碼可讀性高。在這種情況下,所花費(fèi)的時(shí)間大約是iterrows方法的一半。apply()將在內(nèi)部嘗試循環(huán)遍歷Cython迭代器。但是在這種情況下,傳遞的lambda不是可以在Cython中處理的東西,因此它在Python中調(diào)用并不是那么快。apply()方法獲取10年的小時(shí)數(shù)據(jù),那么將需要大約15分鐘的處理時(shí)間。如果這個(gè)計(jì)算只是大規(guī)模計(jì)算的一小部分,那么真的應(yīng)該提速了。這也就是矢量化操作派上用場的地方。三、矢量化操作:使用.isin選擇數(shù)據(jù)
df ['energy_kwh'] * 28,類似這種。那么這個(gè)特定的操作就是矢量化操作的一個(gè)例子,它是在pandas中執(zhí)行的最快方法。pandas中的矢量化運(yùn)算?DataFrame,然后對每個(gè)選定的組應(yīng)用矢量化操作。pandas的.isin()方法選擇行,然后在矢量化操作中實(shí)現(xiàn)新特征的添加。在執(zhí)行此操作之前,如果將date_time列設(shè)置為DataFrame的索引,會(huì)更方便:# 將date_time列設(shè)置為DataFrame的索引
df.set_index('date_time', inplace=True)
@timeit(repeat=3, number=100)
def apply_tariff_isin(df):
# 定義小時(shí)范圍Boolean數(shù)組
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函數(shù)中的定義
df.loc[peak_hours, 'cost_cents'] = df.loc[peak_hours, 'energy_kwh'] * 28
df.loc[shoulder_hours,'cost_cents'] = df.loc[shoulder_hours, 'energy_kwh'] * 20
df.loc[off_peak_hours,'cost_cents'] = df.loc[off_peak_hours, 'energy_kwh'] * 12
>>> apply_tariff_isin(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_isin` ran in average of 0.010 seconds.
.isin()方法返回的是一個(gè)布爾值數(shù)組,如下:[False, False, False, ..., True, True, True]
DataFrame索引datetimes是否落在了指定的小時(shí)范圍內(nèi)。然后把這些布爾數(shù)組傳遞給DataFrame的.loc,將獲得一個(gè)與這些小時(shí)匹配的DataFrame切片。然后再將切片乘以適當(dāng)?shù)馁M(fèi)率,這就是一種快速的矢量化操作了。apply_tariff(),代碼大大減少,同時(shí)速度起飛。四、還能更快?
apply_tariff_isin中,我們通過調(diào)用df.loc和df.index.hour.isin三次來進(jìn)行一些手動(dòng)調(diào)整。如果我們有更精細(xì)的時(shí)間范圍,你可能會(huì)說這個(gè)解決方案是不可擴(kuò)展的。但在這種情況下,我們可以使用pandas的pd.cut()函數(shù)來自動(dòng)完成切割:@timeit(repeat=3, number=100)
def apply_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()會(huì)根據(jù)bin列表應(yīng)用分組。include_lowest參數(shù)表示第一個(gè)間隔是否應(yīng)該是包含左邊的。>>> apply_tariff_cut(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_cut` ran in average of 0.003 seconds.
NumPy,還可以更快!五、使用Numpy繼續(xù)加速
pandas時(shí)不應(yīng)忘記的一點(diǎn)是Pandas的Series和DataFrames是在NumPy庫之上設(shè)計(jì)的。并且,pandas可以與NumPy陣列和操作無縫銜接。NumPy的 digitize()函數(shù)更進(jìn)一步。它類似于上面pandas的cut(),因?yàn)閿?shù)據(jù)將被分箱,但這次它將由一個(gè)索引數(shù)組表示,這些索引表示每小時(shí)所屬的bin。然后將這些索引應(yīng)用于價(jià)格數(shù)組:@timeit(repeat=3, number=100)
def apply_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函數(shù)一樣,這種語法非常簡潔易讀。>>> apply_tariff_digitize(df)
Best of 3 trials with 100 function calls per trial:
Function `apply_tariff_digitize` ran in average of 0.002 seconds.
評論
圖片
表情
