[Python3] Pandas v1.0 —— (九) 高性能Pandas: eval()與query()


[ Pandas version: 1.0.1 ]


十二、高性能Pandas:eval()與query()

Pandas數據科學生態環境的強大力量建立在NumPy與Pandas的基礎上,並通過直觀的語法將基本操作轉換成C語言:在NumPy裏是向量化/廣播運算,在Pandas裏是分組型的運算。

雖然這些抽象功能可以簡潔高效地解決許多問題,但是它們經常需要創建臨時中間對象,這樣就會佔用大量的計算時間與內存。

Pandas從0.13版本開始就引入了實驗性工具,讓用戶可以直接運行C語言速度的操作,不需要十分費力地配置中間數組。它們就是eval()query()函數,都依賴於Numexpr程序包。

(一)query()與eval()的設計動機:複合代數式

NumPy和Pandas快速向量化運算比普通Python循環或列表綜合要快很多:

# 對兩個數組進行求和
import numpy as np
rng = np.random.RandomState(42)
x = rng.rand(int(1E6))
y = rng.rand(int(1E6))
%timeit x + y
# 3.54 ms ± 172 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

%timeit np.fromiter((xi + yi for xi, yi in zip(x, y)), dtype=x.dtype, count=len(x))
# 470 ms ± 29.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

但這種運算在處理複合代數式(compound expression)問題時效率比較低:每段中間過程都需要顯式地分配內存

如果x數組和y數組非常大,運算就會佔用大量的時間和內存消耗。

mask = (x > 0.5) & (y < 0.5)

# 由於NumPy會計算每一個代數子式,計算過程等價於:
# tmp1 = (x > 0.5)
# tmp2 = (y < 0.5)
# mask = tmp1 & tmp2

Numexpr程序包可以在不爲中間過程分配全部內存的前提下,完成元素到元素的複合代數式運算(用一個NumPy風格的字符串代數式進行運算)

  • 優點:Numexpr在計算代數式時不需要爲臨時數組分配全部內存,因此計算比NumPy更高效,尤其適用處理大型數組
  • Pandas的eval()query()工具也是基於Numexpr實現的
import numexpr
mask_numexpr = numexpr.evaluate('(x > 0.5) & (y < 0.5)')
np.allclose(mask, mask_numexpr)
# True

(二)用pandas.eval()實現高性能運算

Pandas的eval()函數用字符串代數式實現了DataFrame的高性能運算:

import pandas as pd
nrows, ncols = 100000, 100
rng = np.random.RandomState(42)
df1, df2, df3, df4 = (pd.DataFrame(rng.rand(nrows, ncols)) for i in range(4))

# 普通Pandas方法計算四個DataFrame的和
%timeit df1 + df2 + df3 + df4
# 93.4 ms ± 8.72 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

# pd.eval和字符串代數式計算並得出相同結果(比普通方法快一倍且內存消耗更少,結果也相同)
%timeit pd.eval('df1 + df2 + df3 + df4')
# 49.4 ms ± 3.82 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

np.allclose(df1 + df2 + df3 + df4, pd.eval('df1 + df2 + df3 + df4'))
# True

pd.eval()支持的運算

# 創建一個整數類型的DataFrame
df1, df2, df3, df4, df5 = (pd.DataFrame(rng.randint(0, 100, (100, 3))) for i in range(5))
(1) 算術運算符

pd.eval()支持所有的算術運算符

result1 = -df1 * df2 / (df3 + df4) - df5
result2 = pd.eval('-df1 * df2 / (df3 + df4) - df5')
np.allclose(result1, result2)
# True
(2) 比較運算符

pd.eval()支持所有的比較運算符+, -, *, /, **, %, //,包括鏈式代數式(chained expression)

result1 = (df1 < df2) & (df2 <= df3) & (df3 != df4)
result2 = pd.eval('df1 < df2 <= df3 != df4')
np.allclose(result1, result2)
# True
(3) 位運算符

pd.eval()支持| (or), & (and), ~ (not)等位運算符,另外還可以在布爾類型的代數式中使用andor等字面值

result1 = (df1 < 0.5) & (df2 < 0.5) | (df3 < df4)
result2 = pd.eval('(df1 < 0.5) & (df2 < 0.5) | (df3 < df4)')
np.allclose(result1, result2)
# True

result3 = pd.eval('(df1 < 0.5) and (df2 < 0.5) or (df3 < df4)')
np.allclose(result1, result3)
# True
(4) 對象屬性與索引

pd.eval()可以通過obj.attr語法獲取對象屬性,通過obj[index]語法獲取對象索引

result1 = df2.T[0] + df3.iloc[1]
result2 = pd.eval('df2.T[0] + df3.iloc[1]')
np.allclose(result1, result2)
# True
(5) 其他運算

pd.eval()暫不支持函數調用、條件語句、循環以及更復雜的運算(但可藉助Numexpr實現)。

(三)用DataFrame.eval()實現列間運算

由於pd.eval()是Pandas的頂層函數,因此DataFrame有一個eval()方法可以做類似的運算。

使用eval()方法的好處是可以藉助列名稱進行運算:

df = pd.DataFrame(rng.rand(1000, 3), columns=['A', 'B', 'C'])
df.head()
#           A         B         C
# 0  0.374540  0.950714  0.731994
# 1  0.598658  0.156019  0.155995
# 2  0.058084  0.866176  0.601115
# 3  0.708073  0.020584  0.969910
# 4  0.832443  0.212339  0.181825

# 用od.eval()可以通過下面代數式計算這三列
result1 = (df['A'] + df['B']) / (df['C'] - 1)
result2 = pd.eval('(df.A + df.B) / (df.C - 1)')
np.allclose(result1, result2)
# True

# 用DataFrame.eval()方法可以通過列名稱實現簡潔的代數式
result3 = df.eval('(A + B) / (C - 1)')
np.allclose(result1, result3)
# True

1. 用DataFrame.eval()新增列

DataFrame.eval()還可以創建新的列。

df.head()
#           A         B         C
# 0  0.374540  0.950714  0.731994
# 1  0.598658  0.156019  0.155995
# 2  0.058084  0.866176  0.601115
# 3  0.708073  0.020584  0.969910
# 4  0.832443  0.212339  0.181825

# 可以用df.eval()創建一個新列'D'並賦給它其他列計算的值
df.eval('D = (A + B) / C', inplace=True)
df.head()
#           A         B         C         D
# 0  0.374540  0.950714  0.731994  1.810472
# 1  0.598658  0.156019  0.155995  4.837844
# 2  0.058084  0.866176  0.601115  1.537576
# 3  0.708073  0.020584  0.969910  0.751263
# 4  0.832443  0.212339  0.181825  5.746085

# 可以修改已有的列
df.eval('D = (A - B) / C', inplace=True)
df.head()
#           A         B         C         D
# 0  0.374540  0.950714  0.731994 -0.787130
# 1  0.598658  0.156019  0.155995  2.837535
# 2  0.058084  0.866176  0.601115 -1.344323
# 3  0.708073  0.020584  0.969910  0.708816
# 4  0.832443  0.212339  0.181825  3.410442

2. DataFrame.eval()使用局部變量

DataFrame.eval()方法還支持通過@符號使用Python的局部變量:

  • @符號表示“這是一個變量名稱而不是一個列名稱”,從而靈活用兩個“命名空間”的資源(列名稱的命名空間和Python對象的命名空間)計算代數式
  • 注意:@符號只能在DataFrame.eval()方法中使用,而不能在pd.eval()函數中使用,因爲pd.eval()函數只能獲取一個Python命名空間的內容
column_mean = df.mean(1)
result1 = df['A'] + column_mean
result2 = df.eval('A + @column_mean')
np.allclose(result1, result2)
# True

(四)DataFrame.query()方法

DataFrame基於字符串代數式的運算實現了另一個方法,稱爲query()

  • query()方法也支持用@符號引用局部變量
result1 = df[(df.A < 0.5) & (df.B < 0.5)]
result2 = pd.eval('df[(df.A < 0.5) & (df.B < 0.5)]')
np.allclose(result1, result2)
# True

# 這是用DataFrame列創建的代數式,但不能用`DataFrame.eval()`語法
# 對於這種過濾運算可以用query()方法

result2 = df.query('A < 0.5 and B < 0.5')
np.allclose(result1, result2)
# True

Cmean = df['C'].mean()
result1 = df[(df.A < Cmean) & (df.B < Cmean)]
result2 = df.query('A < @Cmean and B < @Cmean')
np.allclose(result1, result2)
# True

(五)性能決定使用時機

在考慮要不要用這兩個函數時,需要思考兩個方面:計算時間和內存消耗,而內存消耗是更重要的影響因素。

  • 每個涉及NumPy數組或Pandas的DataFrame的複合代數式都會產生臨時數組
x = df[(df.A < 0.5) & (df.B < 0.5)]

# 它基本等價於:
# tmp1 = df.A < 0.5
# tmp2 = df.B < 0.5
# tmp3 = tmp1 & tmp2
# x = df[tmp3]
  • 如果臨時DataFrame的內存需求比你的系統內存還大,那麼最好還是使用eval()query()代數式
# 可以通過這個方法大概估算變量的內存消耗:
df.values.nbytes
# 32000

在性能方面,即使沒有使用最大的系統內存,eval()的計算速度也比普通方法快。

  • 現在的性能瓶頸變成了臨時DataFrame與系統CPU的L1和L2緩存之間的對比,如果系統緩存足夠大,那麼eval()就可以避免在不同緩存間緩慢地移動臨時文件
  • 在實際工作中,普通的計算方法與eval/query計算方法在計算時間上的差異並非總是那麼明顯,普通方法在處理較小的數組時反而速度更快

eval/query方法的優點主要是節省內存,有時語法也更加簡潔。


Pandas 相關閱讀:

[Python3] Pandas v1.0 —— (一) 對象、數據取值與運算
[Python3] Pandas v1.0 —— (二) 處理缺失值
[Python3] Pandas v1.0 —— (三) 層級索引
[Python3] Pandas v1.0 —— (四) 合併數據集
[Python3] Pandas v1.0 —— (五) 累計與分組
[Python3] Pandas v1.0 —— (六) 數據透視表
[Python3] Pandas v1.0 —— (七) 向量化字符串操作
[Python3] Pandas v1.0 —— (八) 處理時間序列
[Python3] Pandas v1.0 —— (九) 高性能Pandas: eval()與query() 【本文】


總結自《Python數據科學手冊》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章