文章目錄
[ 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)
等位運算符,另外還可以在布爾類型的代數式中使用and
和or
等字面值
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
- 用列名稱作爲變量計算代數式同樣可行
- pandas.DataFrame.eval - pandas 1.0.1 documentation
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數據科學手冊》