- 登入
- 註冊
隨著 Python 越來越普及,許多讀者在求學階段就對 Python 有些著墨,甚至是使用 Python 來完成學校作業。但其實,除了資訊科系以外,大部分 Python 初學者的程式只能說是「跑得出結果」,但是效率是有明顯改善空間的。
確實,Python 在眾多程式語言當中,本身速度就沒有優勢,但我們可以透過一些小技巧,大幅縮短程式運行時間。
本篇主要講述的是 for 迴圈的設計,以及龐大數據量的 pandas 資料索引的優化。
首先,這裡介紹一個很常見的情境,比較一下不同做法的速度:
假設我們要對一個資料長度超過 100 萬的一分線價量資料 DataFrame,比對每筆收盤價有沒有比當根 K 棒開盤價高。(這通常會出現在我們要計算策略訊號的時候)
首先,先讀取資料:
import pandas as pd
price = pd.read_csv(“BTCUSDT.csv”, index_col=”timestamp”)
讀取進來的資料長相為:
print(price) # DataFrame長寬: 1164743 rows x 5 columns |
timestamp | open | high | low | close | volume |
2020/1/1 00:00 | 7170.25 | 7170.5 | 7157 | 7163.25 | 4.65E+05 |
2020/1/1 00:01 | 7163.25 | 7163.75 | 7161.25 | 7161.75 | 1.50E+04 |
2020/1/1 00:02 | 7159.5 | 7161.5 | 7155.5 | 7157.75 | 3.87E+05 |
2020/1/1 00:03 | 7157.75 | 7162.5 | 7157.25 | 7162.5 | 5.35E+05 |
2020/1/1 00:04 | 7160.75 | 7160.75 | 7157.25 | 7158.25 | 2.20E+05 |
… | … | … | … | … | … |
2022/3/19 23:56 | 42236 | 42242 | 42225 | 42242 | 1.06E+06 |
2022/3/19 23:57 | 42242 | 42245 | 42228 | 42236 | 6.86E+05 |
2022/3/19 23:58 | 42236 | 42263 | 42232 | 42232 | 7.39E+05 |
2022/3/19 23:59 | 42232 | 42249 | 42231 | 42238 | 5.12E+05 |
2022/3/20 00:00 | 42238 | 42238 | 42209 | 42209 | 5.25E+05 |
要比對每筆資料,一般初學者往往會很直覺地寫出這個解法:
for i in range(len(price)):
if price.iloc[i]["close"] > price.iloc[i]["open"]:
movement = "rise"
elif price.iloc[i]["close"] < price.iloc[i]["open"]:
movement = "drop"
else:
movement = "flat"
這個解法在學校應該沒什麼問題,放著一個晚上總是跑得完。實際上,我們實測這段程式碼花了 277.03 秒。這速度都超過一碗泡麵的時間了!
那這段程式碼的問題在哪?
首先,for 迴圈我們用 range 每圈對應到「第 i 行」,這個迴圈的寫法注定讓後面取用資料的時候,運行異常緩慢。再來,在迴圈內,我們調用了四次 price 整個 DataFrame 來找到該行的收盤價和開盤價的值。每一次這種調用不會多花多少時間,但是每一圈都有四次調用,而我們有 1,164,743 圈,累積下來的時間花費就變得非常可觀!
所以,我們接著來修正看看,如果每圈只調用兩次 price 一整份的 DataFrame,可以節省多少時間?
for i in range(len(price)):
close_price = price.iloc[i]["close"]
open_price = price.iloc[i]["open"]
if close_price > open_price:
movement = "rise"
elif close_price < open_price:
movement = "drop"
else:
movement = "flat"
結果總花費時間為 179.66 秒。相比節省了將近 100 秒,也就是 36% 的時間!
如果我們再優化,不要用 iloc 的方式,改用 index 做 for 迴圈,效率有改善嗎?
for index in price.index:
close_price = price.loc[index, "close"]
open_price = price.loc[index, "open"]
if close_price > open_price:
movement = "rise"
elif close_price < open_price:
movement = "drop"
else:
movement = "flat"
程式碼優化如上方區塊紅色字體的部分。實測結果,我們在 20.19 秒就運行完成了!儘管每一圈迴圈都要調用兩次 price,但顯然透過 loc,比 iloc 查找資料的運行速度快得多了!
如果是透過許多初學者愛用的 iterrows 來遍歷每一行呢?
for index, row in price.iterrows():
if row["close"] > row["open"]:
movement = "rise"
elif row["close"] < row["open"]:
movement = "drop"
else:
movement = "flat"
實測結果為 65.86 秒,比 iloc 快,但沒有 loc 快。
但其實,Python 最強王牌在這裡:向量化(Vectorize)運算!
price["movement"] = pd.Series(
np.where(price["close"] > price["open"], "rise", "drop"),
index = price.index
)
price.loc[price["close"] == price["open"], "movement"] = "flat"
這裡講解一下程式碼:我們直接在 price 這個 DataFrame 新增一個 column 叫做“movement”,定義這欄的值是用 np.where 判斷收盤價是否大於開盤價的結果,若收盤價大於開盤價,則存為“rise”。若否,則存為“drop”。把這個 np.where 輸出的 array 轉成 pd.Series 格式,就能準確對齊 price 的 index 存入了!
不過要注意,收盤價與開盤價比較,不只有上升、下跌,還有平盤。因此,要另外寫一行,把收盤價大於開盤價的 movement 的值都要改為“flat”,這才會是正確答案。
Python 有很多小撇步可以改善運行效率,向量化是一個非常強大的武器!雖然有時候無法避免使用迴圈,但光是迴圈設計的不同,就有明顯速度差異了。如果有其他更有效率的運行方式,歡迎私訊或留言讓我們知道哦!讓我們一起成長下去吧!