Pandas初學者程式碼優化指南

發表於2017-11-23

摘要:Pandas 是Python Data Analysis Library的簡寫,它是為了解決資料分析任務而建立的工具,本文介紹了五種由慢到快逐步優化其效率的方法 ,以下是譯文

如果你用Python語言做過任何的資料分析,那麼可能會用到Pandas,一個由Wes McKinney寫的奇妙的分析庫。通過賦予Python資料幀以分析功能,Pandas已經有效地把Python和一些諸如R或者SAS這樣比較成熟的分析工具置於相同的地位。

不幸的是,在早期,Pandas因“慢”而聲名狼藉。的確,Pandas程式碼不可能達到如完全優化的原始C語言程式碼的計算速度。然而,好訊息是,對於大多數應用程式來說,寫的好的Pandas程式碼已足夠快;Pandas強大的功能和友好的使用者體驗彌補了其速度的缺點。

在這篇文章中,我們將回顧應用於Pandas DataFrame函式的幾種方法的效率,從最慢到最快:

1. 在用索引的DataFrame行上的Crude looping
2. 用iterrows()迴圈
3. 用 apply()迴圈
4. Pandas Series向量化
5. NumPy陣列向量化

對於我們的例項函式,將使用Haversine(半正矢)距離公式。函式取兩點的經緯度,調整球面的曲率,計算它們之間的直線距離。這個函式看起來像這樣:

為了在真實資料上測試函式,我們用一個包含紐約所有酒店座標的資料集,該資料來自Expedia開發者網站。要計算每一個酒店和一個樣本集座標之間的距離(這恰好屬於在紐約市名為布魯克林超級英雄供應店的一個夢幻般的小商店)

大家可以下載資料集,Jupyter notebook(是一個互動式筆記本,支援執行 40 多種程式語言)包含了用於這篇部落格的函式,請點選這裡下載。

這篇文章基於我的PyCon訪談,大家可以在這裡觀看。

Pandas中的Crude looping,或者你永遠不應該這麼做

首先,讓我們快速回顧一下Pandas資料結構的基本原理。Pandas的基本結構有兩種形式:DataFrameSeries。一個DataFrame是一個二維陣列標記軸,很多功能與R中的data.frame類似,可以將DataFrame理解為Series的容器。換句話說,一個DataFrame是一個有行和列的矩陣,列有列名標籤,行有索引標籤。在Pandas DataFrame中一個單獨的列或者行是一個Pandas Series—一個帶有軸標籤的一維陣列。

幾乎每一個與我合作過的Pandas初學者,都曾經試圖通過一次一個的遍歷DataFrame行去應用自定義函式。這種方法的優點是,它是Python物件之間互動的一致方式;例如,一種可以通過列表或陣列迴圈的方式。反過來說,不利的一面是,在Pandas中,Crude loop是最慢的方法。與下面將要討論的方法不同,Pandas中的Crude loop沒有利用任何內建優化,通過比較,其效率極低(而且程式碼通常不那麼具有可讀性)

例如,有人可能會寫像下面這樣的程式碼:

為了瞭解執行上述函式所需要的時間,我們用%timeit命令。%timeit是一個“神奇的”命令,專用於Jupyter notebook(所有的魔法命令都以%標識開始,如果%命令只應用於一行,那麼%%命令應用於整個Jupyter單元)。%timeit命令將多次執行一個函式,並列印出獲得的執行時間的平均值和標準差。當然,通過%timeit命令獲得的執行時間,執行該函式的每個系統都不盡相同。儘管如此,它可以提供一個有用的基準測試工具,用於比較同一系統和資料集上不同函式的執行時間。

結果是:

通過分析,crude looping函式執行了大約645ms,標準差是31ms。這似乎很快,但考慮到它僅需要處理大約1600行的程式碼,因此它實際上是很慢的。接下來看看如何改善這種不好的狀況。

用iterrows()迴圈

如果迴圈是必須的,找一個更好的方式去遍歷行,比如用iterrows()方法。iterrows()是一個生成器,遍歷DataFrame的所有行並返回每一行的索引,除了包含行自身的物件。iterrows() 是用Pandas DataFrame優化,儘管它是執行大多數標準函式最不高效的方式(稍後再談),但相對於Crude looping,這是一個重大的改進。在我們的案例中,iterrows()解決同一個問題,幾乎比手動遍歷行快四倍。

使用apply()方法實現更好的迴圈

一個比iterrows()更好的選擇是用 apply() 方法,它應用一個函式,沿著DataFrame某一個特定的軸線(意思就是行或列)。雖然apply()也固有的通過行迴圈,但它通過採取一些內部優化比iterrows()更高效,例如在Cython中使用迭代器。我們使用一個匿名的lambda函式,每一行都用Haversine函式,它允許指向每一行中的特定單元格作為函式的輸入。為了指定Pandas是否應該將函式應用於行(axis = 1)或列(axis = 0),Lambda函式包含最終的axis引數。

iterrows()方法用apply()方法替代後,大致可以將函式的執行時間減半。為了更深入地瞭解函式中的實際執行時間,可以執行一個線上分析器工具(Jupyter中神奇的命令%lprun

結果如下:

Pandas初學者程式碼優化指南

我們可以從這個資訊中得到一些有用的見解。例如,進行三角計算的函式佔了總執行時間的近一半。因此,如果想優化函式的各個元件,可以從這裡入手。現在,特別值得注意的是每一行都被迴圈了1631次—apply()遍歷每一行的結果。如果可以減少重複的工作量,就可以降低整個執行時間。向量化提供了一種更有效的替代方案。

Pandas Series向量化

要了解如何可以減少函式所執行的迭代數量,就要記得Pandas的基本單位,DataFrame和Series,它們都基於陣列。基本單元的固有結構轉換成內建的設計用於對整個陣列進行操作的Pandas函式,而不是按各個值的順序(簡稱標量)。向量化是對整個陣列執行操作的過程。

Pandas包含一個總體的向量化函式集合,從數學運算到聚合和字串函式(可用函式的擴充套件列表,檢視Pandas docs)。對Pandas Series和DataFrame的操作進行內建優化。結果,使用向量Pandas函式幾乎總是會用自定義的迴圈實現類似的功能。

到目前為止,我們僅傳遞標量給Haversine函式。所有的函式都應用在Haversine函式中,也可以在陣列上操作。這使得距離向量化函式的過程非常的簡單:不是傳遞個別標量值的緯度和經度給它,而是把它傳遞給整個series(列)。這使得Pandas受益於可用於向量函式的全套優化,特別是包括同時執行整個陣列的所有計算。

通過使用apply()方法,要比用iterrows()方法改進50倍的效率,通過向量化函式則改進了iterrows()方法100倍—除了改變輸入型別,什麼都不要做!

看一眼後臺,看看函式到底在做什麼:

Pandas初學者程式碼優化指南

注意,鑑於 apply() 執行函式1631次,向量化版本僅執行一次,因為它同時應用於整個陣列,這就是主要的時間節省來源。

用NumPy陣列向量化

Pandas series向量化可以完成日常計算優化的絕大多數需要。然而,如果速度是最高優先順序,那麼可以以NumPy Python庫的形式呼叫援軍。

NumPy庫,將自己描述為一個“Python科學計算的基本包”,在後臺執行優化操作,預編譯C語言程式碼。跟Pandas一樣,NumPy運算元組物件(簡稱ndarrays);然而,它省去了Pandas series操作所帶來的大量資源開銷,如索引、資料型別檢查等。因此,NumPy陣列的操作可以明顯快於pandas series的操作。

當Pandas series提供的額外功能不是很關鍵的時候,NumPy陣列可以用於替代Pandas series。例如,Haversine函式向量化實現不使用索引的經度和緯度系列,因此沒有那些索引,也不會導致函式中斷。通過比較,我們所做的操作如DataFrame的連線,它需要按索引來引用值,可能需要堅持使用Pandas物件。

僅僅是使用Pandas series 的values的方法,把緯度和經度陣列從Pandas series轉換到NumPy陣列。就像series向量化一樣,通過NumPy陣列直接進入函式將可以讓Pandas對整個向量應用函式。

NumPy陣列操作執行取得了又一個四倍的改善。總之,通過looping改進了執行時間超過半秒,通過NumPy向量化,執行時間改進到了三分之一毫秒級!


總結

下面的表格總結了相關結果。用NumPy陣列向量化將會帶來最快的執行時間,相對於Pandas series向量化的效果而言,這是一個很小的改進,但對比最快的looping版本,NumPy陣列向量化帶來了56倍的改進。

Pandas初學者程式碼優化指南

這給我們帶來了一些關於優化Pandas程式碼的基本結論:

  1. 避免迴圈;它們很慢,而且在大多數情況下是不必要的。
  2. 如果必須使用迴圈,用 apply(),而不是迭代函式。
  3. 向量化通常優於標量運算。在Pandas中的大部分常見操作都可以向量化。
  4. NumPy陣列向量化操作比Pandas series更有效。

當然,以上並不是Pandas所有可能優化的全面清單。更愛冒險的使用者或許可以考慮進一步用Cython改寫函式,或者嘗試優化函式的各個元件。然而,這些話題超出了這篇文章的範圍。

關鍵的是,在開始一次巨集大的優化冒險之前,要確保正在優化的函式實際上是你希望在長期執行中使用的函式。引用XKCD不朽的名言:“過早優化是萬惡之源”。

相關文章