再見 for 循環(huán)!pandas 提速 315 倍~
點擊上方Python知識圈,設(shè)為星標(biāo)
回復(fù)100獲取100題PDF
閱讀文本大概需要 5?分鐘
來源:Python數(shù)據(jù)科學(xué)
作者:東哥起飛

for是所有編程語言的基礎(chǔ)語法,初學(xué)者為了快速實現(xiàn)功能,依懶性較強。但如果從運算時間性能上考慮可能不是特別好的選擇。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方法寫一個函數(shù),函數(shù)里面寫好時間條件的邏輯代碼。
def?apply_tariff(kwh,?hour):
????"""計算每個小時的電費"""????
????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)計算enery?cost,并添加到列表"""
...?????energy_cost_list?=?[]
...?????for?i?in?range(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)
Best?of?3?trials?with?100?function?calls?per?trial:
Function?`apply_tariff_loop`?ran?in?average?of?3.152?seconds.
Pythonic風(fēng)格的人來說,這個設(shè)計看起來很自然。然而,這個循環(huán)將會嚴(yán)重影響效率。原因有幾個:(0,len(df))循環(huán),然后再應(yīng)用apply_tariff()之后,它必須將結(jié)果附加到用于創(chuàng)建新DataFrame列的列表中。另外,還使用df.iloc [i]['date_time']執(zhí)行所謂的鏈?zhǔn)剿饕?,這通常會導(dǎo)致意外的結(jié)果。一、使用 iterrows循環(huán)
pandas引入iterrows方法讓效率更高。這些都是一次產(chǎn)生一行的生成器方法,類似scrapy中使用的yield用法。.itertuples為每一行產(chǎn)生一個namedtuple,并且行的索引值作為元組的第一個元素。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():
...?????????#?獲取用電量和時間(小時)
...?????????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進一步改進此操作。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)點很明顯,行數(shù)少,代碼可讀性高。在這種情況下,所花費的時間大約是iterrows方法的一半。apply()將在內(nèi)部嘗試循環(huán)遍歷Cython迭代器。但是在這種情況下,傳遞的lambda不是可以在Cython中處理的東西,因此它在Python中調(diào)用并不是那么快。apply()方法獲取10年的小時數(shù)據(jù),那么將需要大約15分鐘的處理時間。如果這個計算只是大規(guī)模計算的一小部分,那么真的應(yīng)該提速了。這也就是矢量化操作派上用場的地方。三、矢量化操作:使用.isin選擇數(shù)據(jù)
df ['energy_kwh'] * 28,類似這種。那么這個特定的操作就是矢量化操作的一個例子,它是在pandas中執(zhí)行的最快方法。pandas中的矢量化運算?DataFrame,然后對每個選定的組應(yīng)用矢量化操作。pandas的.isin()方法選擇行,然后在矢量化操作中實現(xiàn)新特征的添加。在執(zhí)行此操作之前,如果將date_time列設(shè)置為DataFrame的索引,會更方便:#?將date_time列設(shè)置為DataFrame的索引
df.set_index('date_time',?inplace=True)
@timeit(repeat=3,?number=100)
def?apply_tariff_isin(df):
????#?定義小時范圍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()方法返回的是一個布爾值數(shù)組,如下:[False,?False,?False,?...,?True,?True,?True]
DataFrame索引datetimes是否落在了指定的小時范圍內(nèi)。然后把這些布爾數(shù)組傳遞給DataFrame的.loc,將獲得一個與這些小時匹配的DataFrame切片。然后再將切片乘以適當(dāng)?shù)馁M率,這就是一種快速的矢量化操作了。apply_tariff(),代碼大大減少,同時速度起飛。四、還能更快?
apply_tariff_isin中,我們通過調(diào)用df.loc和df.index.hour.isin三次來進行一些手動調(diào)整。如果我們有更精細的時間范圍,你可能會說這個解決方案是不可擴展的。但在這種情況下,我們可以使用pandas的pd.cut()函數(shù)來自動完成切割:@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()會根據(jù)bin列表應(yīng)用分組。include_lowest參數(shù)表示第一個間隔是否應(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時不應(yīng)忘記的一點是Pandas的Series和DataFrames是在NumPy庫之上設(shè)計的。并且,pandas可以與NumPy陣列和操作無縫銜接。NumPy的?digitize()函數(shù)更進一步。它類似于上面pandas的cut(),因為數(shù)據(jù)將被分箱,但這次它將由一個索引數(shù)組表示,這些索引表示每小時所屬的bin。然后將這些索引應(yīng)用于價格數(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.
往期推薦 01 02 03
↓點擊閱讀原文查看pk哥原創(chuàng)視頻
我就知道你“在看” 

評論
圖片
表情
