14.Pandas高效能運算:eval()與query()

2020-08-08 13:31:16

Pandas的高效能運算:eval()與query()

Numpy與Pandas的底層實際上都是用C語言寫成的併爲Python預留了介面.

Numpy強大的能力來源於向量化運算和廣播功能,而Pandas的強大能力來源於分組型運算

這些抽象的規則在賦予這兩大工具強大的處理能力時,卻也造成了一個困難:在進行處理時Pandas與Numpy經常會建立臨時的中間物件

因此,Pandas從0.13版本(2014年1月發佈)開始引入了實驗性工具,允許使用者直接以C語言速度的來執行程式,並且不需要費力的設定中間陣列

即eval()和query()函數,他們都位於Numexpr庫中


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

前面已經介紹過,Pandas和Numpy都支援快速的向量化運算

但是Pandas與Numpy支援的向量化運算的前提就是爲所有參與運算的物件顯式的分配記憶體

例如

import numpy

x=np.random.randint(0,10000,10000)
y=np.random.randint(0,10000,10000)
mask=(x > 500) & (y < 500)

Numpy處理上面的程式的步驟是:

  1. 爲x和y分配記憶體並且生成亂數組
  2. 分配中間陣列temp_1儲存x>500的結果
  3. 分配中間陣列temp_2儲存y<500的結果
  4. 爲mask分配記憶體並儲存temp_1與temp_2與運算的結果

上面的過程一共爲x,y,temp_1,temp_2,mask共五個陣列分配了大小,最終一共劃分了50000個單元記憶體出去

這在處理大型數據的時候非常的糟糕,所以向量化運算反而會造成程式處理複合代數式時效率的降低

這也就是Numexpr庫創立的動機,就是去彌補Numpy的向量化運算在處理複合代數式時的缺陷

Numexpr的思路就是不爲中間陣列分配記憶體,即直接取x與y對應位置上的元素進行運算後直接儲存到mask中

這樣僅需要分配三個陣列即可

下面 下麪將介紹的Pandas的eval()與query()都是基於Numexpr庫實現的

Pandas.eval()實現高效能運算

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

這裏我們使用time模組的clock()函數來計算Python語句的CPU佔用時間

DataFrame_1=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_2=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_3=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_4=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))

start_1=time.clock()
DataFrame_1+DataFrame_2+DataFrame_3+DataFrame_4
end_1=time.clock()
print(end_1-start_1)

start_2=time.clock()
pd.eval('DataFrame_1+DataFrame_2+DataFrame_3+DataFrame_4')
end_2=time.clock()
print(end_2-start_2)
>>>
0.07319900000000001
0.02056900000000006

我們可以看到,在處理1000×1000的陣列時,使用eval函數就將效能提升了三倍多一點

所以在一般情況下,在處理大型數據時,我們最好使用eval或者query這樣的高效能運算來節約運算時間

Pandas.eval()函數支援的運算

在最初的版本,Pandas.eval()函數支援的運算較少,但是從Pandas 0.16版本以後,Pandas.eval()函數就支援許多運算了,下面 下麪將一一介紹

算術運算子

Pandas.eval()支援所有的算數運算子,但是依舊需要以字串的形式給出

DataFrame_1=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_2=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_3=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_4=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))


start_2=time.clock()
pd.eval('(DataFrame_1*DataFrame_2)/(DataFrame_3-DataFrame_4)')
end_2=time.clock()
print(end_2-start_2)
>>>
0.04514499999999999

比較運算子

Pandas.eval()函數支援所有的比較運算子,包括鏈式代數式

DataFrame_1=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_2=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_3=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_4=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))

start_2=time.clock()
pd.eval('(DataFrame_1<DataFrame_2)&(DataFrame_2<=DataFrame_3)&(DataFrame_3!=DataFrame_4)')
end_2=time.clock()
print(end_2-start_2)
>>>
0.023680999999999952

位運算子

Pandas.eval()函數支援&(按位元與)和|(按位元或)等位運算

DataFrame_1=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_2=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_3=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_4=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))

start_2=time.clock()
pd.eval('(DataFrame_1<DataFrame_2)|(DataFrame_2>DataFrame_3)&(DataFrame_3==DataFrame_4)')
end_2=time.clock()
print(end_2-start_2)
>>>
0.025263000000000035

此外,Pandas.eval()也支援and,or和not等封裝器

物件屬性與索引

Pandas.eval()函數也支援在字串內對DataFrame物件進行索引獲取列或者使用索引器來獲取值

DataFrame_1=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_2=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_3=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_4=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))

start_2=time.clock()
ans=pd.eval('DataFrame_2.T[0]==DataFrame_2.iloc[0]')
end_2=time.clock()
print(end_2-start_2)
>>>
0.04004399999999997

但是Pandas.eval()並不支援在索引器中使用切片

其他運算

最後,再說下Pandas.eval()不支援的運算,包括函數呼叫,條件語句,回圈,切片以及更復雜的運算

這些都可以藉助Numexpr實現

DataFrame.eval()方法實現列間運算

Pandas.eval()是Pandas的頂層函數,但是DataFrame物件中其實還內建了eval()方法來實現Pandas.eval()支援的所有的運算

使用DataFrame物件的eval()方法的好處就是可以藉助列名稱直接進行運算

DataFrame_1=pd.DataFrame(np.random.randint(0,10,(3,4)),columns=list('ABCD'),index=list('abc'))
print(DataFrame_1)
start_2=time.clock()
print(DataFrame_1.eval('A+B-C/D'))
end_2=time.clock()
print(end_2-start_2)
>>>
   A  B  C  D
a  9  6  5  4
b  0  0  8  4
c  6  2  6  9
a    13.750000
b    -2.000000
c     7.333333
dtype: float64
0.007834000000000008

使用DataFrame.eval()方法新增列

實際上我們可以在計算的時候使用新的列名,然後指定eval()方法的inplace參數爲True

DataFrame_1=pd.DataFrame(np.random.randint(0,10,(3,4)),columns=list('ABCD'),index=list('abc'))
print(DataFrame_1)
start_2=time.clock()
DataFrame_1.eval('E=A+B-C/D',inplace=True)
end_2=time.clock()
print(DataFrame_1)
print(end_2-start_2)
>>>
   A  B  C  D
a  8  0  7  5
b  2  8  9  1
c  5  7  2  0

   A  B  C  D    E
a  8  0  7  5  6.6
b  2  8  9  1  1.0
c  5  7  2  0 -inf
0.007609999999999895

DataFrame.query()方法

基於字串代數式,DataFrame實現了query()方法,query()方法主要用於鏈式代數式來實現過濾,即會直接返回符合我們鏈式代數式的行

DataFrame_1=pd.DataFrame(np.random.randint(0,10,(3,4)),columns=list('ABCD'),index=list('abc'))
print(DataFrame_1)
start_2=time.clock()
print(DataFrame_1.query('A<B'))
end_2=time.clock()
print(end_2-start_2)
>>>
   A  B  C  D
a  6  7  4  9
b  6  6  1  4
c  8  9  3  2
   A  B  C  D
a  6  7  4  9
c  8  9  3  2
0.010602999999999918

效能決定時機

實際上我們到底用不用Pandas.eval(),DataFrame.eval()或者DataFrame.query()的關鍵在於:計算時間記憶體消耗

使用上面的三個函數 / 方法是比直接用Numpy的向量化運算要慢的

當處理小型數據集時,爲了追求運算速度,我們可以直接使用Numpy的複合代數式,即便這樣會開闢臨時陣列

當處理大型數據集時,直接使用Numpy的複合代數式開闢的臨時陣列可能會直接擠爆CPU的L1和L2快取,因此我們最好還是使用三個函數 / 方法