0.13版本之後,Pandas增加了一些工具,能夠在沒有任何中間內存開銷的情況下直接獲得C語言級別的運算速度。這就是我們接下來要介紹的兩個函數eval()和query()

先來看一個例子:

import numpy as np
x = rng.rand(1E6)
y = rng.rand(1E6)
?
mask = (x > 0.5) & (y < 0.5)

Numpy會逐個表達式進行運算,因此上面的代碼相當於(頂多中間變數沒有暴露給用戶):

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

每一步的中間計算,都需要分配(allocate)額外的內存,當數據量很大時就會佔有非常大的內存。

Numexpr庫

Numexpr庫,通過在CPU的cache中逐元素的計算,實現無中間內存開銷。只需要傳入一個 Numpy-Style的表達式字元串

import numexpr
?
mask_numexpr = numexpr.evaluate((x > 0.5) & (y < 0.5))

Pandas的eval()與query()

Pandas的eval()和query()與其很類似,其實他們底層都調用了Numexpr包

為了更好的演示,我們先隨機生成4個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))

eval()

符號說明:後續的代碼中我們用

result1代表傳統的操作方法及其結果

result2代表eval/query操作方式及其結果

如果有result3,表示另一種寫法 也屬於eval/query

先演示一個最基本的,我們將四個df對應位置元素相加

result1 = df1 + df2 + df3 + df4 # 標準做法:87.1 ms
result2 = pd.eval(df1 + df2 + df3 + df4) # 42.2 ms

下面我們一一舉例,有哪些操作可以使用eval:

算術運算:

result1 = -df1 * df2 / (df3 + df4) - df5
result2 = pd.eval(-df1 * df2 / (df3 + df4) - df5)

比較運算:

result1 = (df1 < df2) & (df2 <= df3) & (df3 != df4)
result2 = pd.eval(df1 < df2 <= df3 != df4)

Bitwise運算:

result1 = (df1 < 0.5) & (df2 < 0.5) | (df3 < df4)
result2 = pd.eval((df1 < 0.5) & (df2 < 0.5) | (df3 < df4))
result3 = pd.eval((df1 < 0.5) and (df2 < 0.5) or (df3 < df4))

對象屬性和索引:

還可以對DataFrame的某一列進行操作

result1 = df2.T[0] + df3.iloc[1]
result2 = pd.eval(df2.T[0] + df3.iloc[1])

Column-Wise操作

除了有top-level的np.eval(),DataFramse有eval()方法。這點與Pandas的整體範式保持一致

result1 = (df[A] + df[B]) / (df[C] - 1)

result2 = pd.eval("(df.A + df.B) / (df.C - 1)")
result3 = df.eval((A + B) / (C - 1))

通過eval賦值與修改

我們可以通過eval()新建一列數據D

df.eval(D = (A + B) / C, inplace=True)

之後還可以修改D列:

df.eval(D = (A - B) / C, inplace=True)

在eval()中引用 local變數

使用@符號後面跟變數名,就可以引用該變數進行計算

column_mean = df.mean(1)
result1 = df[A] + column_mean
result2 = df.eval(A + @column_mean)

Query

當我們要做的不是修改數據而是篩選數據時,使用query()函數

result1 = df[(df.A < 0.5) & (df.B < 0.5)]
result2 = df.query(A < 0.5 and B < 0.5)

通用,query也可以引用local變數:

Cmean = df[C].mean()
result1 = df[(df.A < Cmean) & (df.B < Cmean)]
result2 = df.query(A < @Cmean and B < @Cmean)

性能:什麼時候考慮使用eval()和query()?

當我們選擇eval()和query()時一般出於兩點考慮:

  1. 計算時間
  2. 內存消耗

內存消耗

內存消耗是非常可預測的,使用以下語句估計數據量的大小(單位為位元組)

df.values.nbytes

計算時間

從運算速度角度,取決於臨時變數的大小有沒有超出CPU的L1或L2 Cache,如果超出了,會有一些緩存之間交換數據的開銷。

結論

實際使用中,傳統方法和eval/query相差不是很明顯,傳統方法在較小的array上更快一些

使用eval/query的好處,主要是節省內存以及有時他們具有更簡潔的語法形式

參考閱讀:

Numexpr documentation

Enhancing Performance

推薦閱讀:

相關文章