正如我們在前面的文章中已經介紹過的,Python 數據科學生態環境的強大力量是建立在 NumPy 和 Pandas 的基礎之上,並通過直觀的語法將基本操作轉換成C語言:
在NumPy中,是向量化/廣播式操作。
在Pandas中是分組式運算。
雖然這些抽象功能對於許多常見的用例來說是簡潔和高效的,但它們往往依賴於臨時中間對象的創建,這樣就會佔用大量的計算時間和內存,造成算力的不必要的浪費。
從0.13版本(2014年1月正式發布)開始,Pandas引入了一些實驗性的工具,允許用戶直接運行C語言速度的操作,而不需要費力地配置中間數組。這些就是eval() 和 query() 函數。
eval()和query()它們依賴於Numexpr程序包。
(https://github.com/pydata/numexpr)。
下面我們將介紹它們的使用方法,並給出我自己做項目中的一些經驗,告訴你何時可以考慮使用它們。
# query() 和 eval() 的設計動機:複合表達式
前面我們已經看到,NumPy 和 Pandas 都支持快速的向量化操作;例如,對下面兩個數組進行求和:
這樣做比普通的Python 循環或列表綜合要快很多:
但這種運算在處理複合表達式的問題時,效率會變得不那麼高效。例如,考慮以下表達式:
因為 NumPy 會對每個代數子表達式進行計算,所以大致等價於以下內容:
換句話說,每一個中間步驟都在內存中顯式分配。
如果x和y數組非常大,會導致大量的內存和計算開銷。
Numexpr 程序庫提供了元素到元素的複合代數式運算,而無需分配完整的中間數組。
Numexpr 文檔中有更多的細節,但簡而言之,這個程序庫其實就是用一個 NumPy 風格的字符串代數式進行運算:
好處是,Numexpr 在計算代數式時不需要為臨時數組分配全部內存,因此可以比 NumPy 更高效,尤其是對於大型數組的處理更為適合。
我們將在下面討論 Pandas 的 eval() 和 query() 工具在概念上是相似的,並且也是基于于 Numexpr 來實現的。
# 用 pandas.eval() 實現高性能運算
Pandas中的 eval() 函數使用字符串表達式實現了 DataFrames 的高性能運算。例如以下的DataFrames:
如果要用普通的 Pandas 方法計算所有四個 DataFrames 的總和,我們可以直接寫出總和:
可以通過 pd.eval 和字符串表達式計算出同樣的結果:
這個表達式的 eval() 版本比普通方法大約快了一倍(而且使用的內存少得多),同時給出了同樣的結果:
## pd.eval() 支持的運算
從Pandas v0.16版本開始,pd.eval()支持多種操作運算。為了演示這些操作,我們將使用一個整數類型的 DataFrames:
### 算術運算符
pd.eval() 支持所有的算術運算符。例如:
### 比較運算符
pd.eval() 支持所有比較運算符,包括鏈式表達式:
### 位運算符
pd.eval() 支持 & 和 | (或)等位運算符:
此外,它還支持在布爾表達式中使用 and 和 or 等字面值:
### 對象屬性與索引
pd.eval() 支持通過 obj.attr 語法獲取對象屬性,以及通過 obj[index] 語法獲取對象索引:
### 其他運算
其他的操作,如函數調用、條件語句、循環和其他更複雜的運算。目前還不能在 pd.eval() 中實現。
如果你想執行這些更複雜的表達式類型,你可以使用Numexpr 庫來實現。
# 用 DataFrame.eval() 實現列間運算
就像 Pandas 有一個頂層的 pd.eval() 函數一樣,DataFrames 也有一個 eval() 方法可以做類似的運算。
使用 eval() 方法的好處是可以藉助列名稱來進行運算。我們以這個標籤數組為例:
如果使用前面介紹的 pd.eval(),我們可以通過下面的代數式計算出這三列:
DataFrame.eval() 方法可以通過列名稱實現更簡潔的表達式:
請注意,這裡我們將列名稱作為變量來計算表達式,結果也是正確的。
## 用DataFrame.eval() 新增列
除了剛才討論的運算功能外,DataFrame.eval() 還允許創建新的列。
讓我們使用之前的 DataFrame 來演示,它有 'A'、'B' 和 'C' 列:
我們可以使用 df.eval() 來創建一個新的列'D',並給它分配一個從其他列計算出來的值:
還可以修改已有的列:
## DataFrame.eval() 使用局部變量
DataFrame.eval() 方法支持一種額外的語法,它允許它與本地 Python 變量一起工作。如下所示:
這裡的 @ 字符表示這是一個變量名,而不是一個列名,它讓你有效地評估兩個 "命名空間 "的表達式:列的命名空間和 Python 對象的命名空間。
注意這個 @ 字符只能在 DataFrame.eval() 方法中使用,而不能在 pandas.eval() 函數中使用,因為 pandas.eval()函數只能獲取一個 (Python) 命名空間的內容。
# DataFrame.query() 方法
DataFrame有另一個基於已評估字符串的方法,稱為query()方法。考慮以下內容:
就像在我們討論中使用的例子一樣DataFrame.eval(),這是一個涉及列的表達式DataFrame。
但是,不能使用DataFrame.eval()語法來表達它!
相反,對於這種類型的過濾操作,可以使用以下query()方法:
與掩蔽表達式相比,除了計算效率更高之外,它更易於閱讀和理解。請注意,該query()方法還接受@標記以標記局部變量:
# 何時使用eval()和query()性能決定使用時機。
在考慮是否使用這些功能時,有兩個注意事項:計算時間和內存使用量。
內存使用是最可預測的方面。如前所述,每個涉及NumPy數組或PandasDataFrame的複合表達式都將導致隱式創建臨時數組。例如:
大致相當於:
如果DataFrame臨時佔用內存比我們電腦可用系統內存要大,(通常為數GB),則最好使用eval() orquery()表達式。
我們可以使用以下方法檢查數組的近似大小(以字節為單位):
在性能方面,eval()即使您沒有最大限度地利用系統內存,也可以更快。
本文介紹了eval()和query()的使用方法。主要是讓計算機的效率達到最高,是Pandas高級用法之一。
關注🐼君,持續學習精進。