# 7.15 高性能 Pandas:eval()
和query()
原文:High-Performance Pandas:
eval()
andquery()
譯者:飛龍
協(xié)議:CC BY-NC-SA 4.0
本節(jié)是《Python 數(shù)據(jù)科學(xué)手冊(cè)》(Python Data Science Handbook)的摘錄汗侵。
我們?cè)谇懊娴恼鹿?jié)中已經(jīng)看到,PyData 技術(shù)棧的力量楣号,建立在 NumPy 和 Pandas 通過直觀語法,將基本操作推送到 C 的能力的基礎(chǔ)上:例如 NumPy 中的向量化/廣播操作,以及 Pandas 的分組類型操作。雖然這些抽象對(duì)于許多常見用例是高效且有效的,但它們通常依賴于臨時(shí)中間對(duì)象的創(chuàng)建,這可能產(chǎn)生計(jì)算時(shí)間和內(nèi)存使用的開銷浅乔。
從版本 0.13(2014 年 1 月發(fā)布)開始倔喂,Pandas 包含一些實(shí)驗(yàn)性工具,允許你直接訪問速度和 C 一樣的操作靖苇,而無需昂貴的中間數(shù)組分配席噩。這些是eval()
和query()
函數(shù),它依賴于 Numexpr 包贤壁。在這個(gè)筆記本中悼枢,我們將逐步介紹它們的使用方法,并提供一些何時(shí)可以考慮使用它們的經(jīng)驗(yàn)法則脾拆。
query()
和eval()
的動(dòng)機(jī):復(fù)合表達(dá)式
我們以前見過 NumPy 和 Pandas 支持快速向量化操作馒索;例如,相加兩個(gè)數(shù)組的元素時(shí):
import numpy as np
rng = np.random.RandomState(42)
x = rng.rand(1000000)
y = rng.rand(1000000)
%timeit x + y
# 100 loops, best of 3: 3.39 ms per loop
正如“NumPy 數(shù)組的計(jì)算:通用函數(shù)”中所討論的名船,這比通過 Python 循環(huán)或推導(dǎo)式執(zhí)行加法要快得多:
%timeit np.fromiter((xi + yi for xi, yi in zip(x, y)), dtype=x.dtype, count=len(x))
# 1 loop, best of 3: 266 ms per loop
但是在計(jì)算復(fù)合表達(dá)式時(shí)绰上,這種抽象可能變得不那么有效。例如渠驼,請(qǐng)考慮以下表達(dá)式:
mask = (x > 0.5) & (y < 0.5)
因?yàn)?NumPy 會(huì)計(jì)算每個(gè)子表達(dá)式蜈块,所以大致相當(dāng)于以下內(nèi)容:
tmp1 = (x > 0.5)
tmp2 = (y < 0.5)
mask = tmp1 & tmp2
換句話說,每個(gè)中間步驟都在內(nèi)存中明確分配。如果x
和y
數(shù)組非常大百揭,這可能會(huì)產(chǎn)生大量?jī)?nèi)存和計(jì)算開銷爽哎。Numexpr 庫使你能夠逐元素計(jì)算這種類型的復(fù)合表達(dá)式,而無需分配完整的中間數(shù)組器一。Numexpr 文檔有更多細(xì)節(jié)课锌,但暫時(shí)可以說,這個(gè)庫接受字符串盹舞,它提供了你想要計(jì)算的 NumPy 風(fēng)格的表達(dá)式:
import numexpr
mask_numexpr = numexpr.evaluate('(x > 0.5) & (y < 0.5)')
np.allclose(mask, mask_numexpr)
# True
這里的好處是产镐,Numexpr 以不使用完整臨時(shí)數(shù)組的方式計(jì)算表達(dá)式,因此可以比 NumPy 更有效踢步,特別是對(duì)于大型數(shù)組癣亚。我們將在這里討論的 Pandas eval()
和query()
工具,在概念上是相似的获印,并且依賴于 Numexpr 包述雾。
用于高效操作的pandas.eval()
Pandas 中的eval()
函數(shù)接受字符串表達(dá)式,來使用DataFrame
高效地計(jì)算操作兼丰。例如玻孟,考慮以下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 方法計(jì)算所有四個(gè)DataFrame
的和,我們可以寫出總和:
%timeit df1 + df2 + df3 + df4
# 10 loops, best of 3: 87.1 ms per loop
通過將表達(dá)式構(gòu)造為字符串鳍征,可以通過pd.eval
計(jì)算相同的結(jié)果:
%timeit pd.eval('df1 + df2 + df3 + df4')
# 10 loops, best of 3: 42.2 ms per loop
這個(gè)表達(dá)式的eval()
版本速度提高了約 50%(并且使用的內(nèi)存更少)黍翎,同時(shí)給出了相同的結(jié)果:
np.allclose(df1 + df2 + df3 + df4,
pd.eval('df1 + df2 + df3 + df4'))
# True
pd.eval()
所支持的操作
從 Pandas v0.16 開始,pd.eval()
支持廣泛的操作艳丛。為了演示這些匣掸,我們將使用以下整數(shù)DataFrame
:
df1, df2, df3, df4, df5 = (pd.DataFrame(rng.randint(0, 1000, (100, 3)))
for i in range(5))
算術(shù)運(yùn)算符
pd.eval()
支持所有算術(shù)運(yùn)算符,例如:
result1 = -df1 * df2 / (df3 + df4) - df5
result2 = pd.eval('-df1 * df2 / (df3 + df4) - df5')
np.allclose(result1, result2)
# True
比較運(yùn)算符
pd.eval()
支持所有比較運(yùn)算符氮双,包括鏈?zhǔn)奖磉_(dá)式:
result1 = (df1 < df2) & (df2 <= df3) & (df3 != df4)
result2 = pd.eval('df1 < df2 <= df3 != df4')
np.allclose(result1, result2)
# True
按位運(yùn)算符
pd.eval()
支持&
和|
按位運(yùn)算符:
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
另外碰酝,它支持在布爾表達(dá)式中使用字面and
和or
:
result3 = pd.eval('(df1 < 0.5) and (df2 < 0.5) or (df3 < df4)')
np.allclose(result1, result3)
# True
對(duì)象屬性和索引
pd.eval()
支持通過obj.attr
語法訪問對(duì)象屬性,和通過obj[index]
語法進(jìn)行索引:
result1 = df2.T[0] + df3.iloc[1]
result2 = pd.eval('df2.T[0] + df3.iloc[1]')
np.allclose(result1, result2)
# True
其它運(yùn)算符
其他操作戴差,如函數(shù)調(diào)用送爸,條件語句,循環(huán)和其他更復(fù)雜的結(jié)構(gòu)暖释,目前都沒有在pd.eval()
中實(shí)現(xiàn)袭厂。如果你想執(zhí)行這些更復(fù)雜的表達(dá)式,可以使用 Numexpr 庫本身球匕。
用于逐列運(yùn)算的DataFrame.eval()
就像 Pandas 有頂級(jí)的pd.eval()
函數(shù)一樣嵌器,DataFrame
有eval()
方法,它的工作方式類似谐丢。eval()
方法的好處是列可以通過名稱引用爽航。我們將使用這個(gè)帶標(biāo)簽的數(shù)組作為示例:
df = pd.DataFrame(rng.rand(1000, 3), columns=['A', 'B', 'C'])
df.head()
A | B | C | |
---|---|---|---|
0 | 0.375506 | 0.406939 | 0.069938 |
1 | 0.069087 | 0.235615 | 0.154374 |
2 | 0.677945 | 0.433839 | 0.652324 |
3 | 0.264038 | 0.808055 | 0.347197 |
4 | 0.589161 | 0.252418 | 0.557789 |
使用上面的pd.eval()
蚓让,我們可以像這樣使用三列來計(jì)算表達(dá)式:
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()
方法允許使用列來更簡(jiǎn)潔地求解表達(dá)式:
result3 = df.eval('(A + B) / (C - 1)')
np.allclose(result1, result3)
# True
請(qǐng)注意,我們將列名稱視為要求解的表達(dá)式中的變量讥珍,結(jié)果是我們希望的結(jié)果历极。
DataFrame.eval()
中的賦值
除了剛才討論的選項(xiàng)之外,DataFrame.eval()
還允許賦值給任何列衷佃。讓我們使用之前的DataFrame
趟卸,它有列A
,B
和C
:
df.head()
A | B | C | |
---|---|---|---|
0 | 0.375506 | 0.406939 | 0.069938 |
1 | 0.069087 | 0.235615 | 0.154374 |
2 | 0.677945 | 0.433839 | 0.652324 |
3 | 0.264038 | 0.808055 | 0.347197 |
4 | 0.589161 | 0.252418 | 0.557789 |
我們可以使用df.eval()
創(chuàng)建一個(gè)新列'D'
并為其賦一個(gè)從其他列計(jì)算的值:
df.eval('D = (A + B) / C', inplace=True)
df.head()
A | B | C | D | |
---|---|---|---|---|
0 | 0.375506 | 0.406939 | 0.069938 | 11.187620 |
1 | 0.069087 | 0.235615 | 0.154374 | 1.973796 |
2 | 0.677945 | 0.433839 | 0.652324 | 1.704344 |
3 | 0.264038 | 0.808055 | 0.347197 | 3.087857 |
4 | 0.589161 | 0.252418 | 0.557789 | 1.508776 |
以同樣的方式氏义,可以修改任何現(xiàn)有列:
df.eval('D = (A - B) / C', inplace=True)
df.head()
A | B | C | D | |
---|---|---|---|---|
0 | 0.375506 | 0.406939 | 0.069938 | -0.449425 |
1 | 0.069087 | 0.235615 | 0.154374 | -1.078728 |
2 | 0.677945 | 0.433839 | 0.652324 | 0.374209 |
3 | 0.264038 | 0.808055 | 0.347197 | -1.566886 |
4 | 0.589161 | 0.252418 | 0.557789 | 0.603708 |
DataFrame.eval()
中的局部變量
DataFrame.eval()
方法支持一種額外的語法锄列,可以使用 Python 局部變量」哂疲考慮以下:
column_mean = df.mean(1)
result1 = df['A'] + column_mean
result2 = df.eval('A + @column_mean')
np.allclose(result1, result2)
# True
這里的@
字符標(biāo)記變量名而不是列名邻邮,并允許你高效計(jì)算涉及兩個(gè)“名稱空間”的表達(dá)式:列的名稱空間和 Python 對(duì)象的名稱空間。請(qǐng)注意克婶,這個(gè)@
字符僅由DataFrame.eval()
方法支持筒严,不由pandas.eval()
函數(shù)支持,因?yàn)?code>pandas.eval ()函數(shù)只能訪問一個(gè)(Python)命名空間情萤。
DataFrame.query()
方法
DataFrame
有另一種基于字符串的求值方法鸭蛙,稱為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.eval()
時(shí)使用的示例一樣娶视,這是一個(gè)涉及DataFrame
列的表達(dá)式。但是睁宰,無法使用DataFrame.eval()
語法表達(dá)它肪获!相反,對(duì)于這種類型的過濾操作勋陪,你可以使用query()
方法:
result2 = df.query('A < 0.5 and B < 0.5')
np.allclose(result1, result2)
# True
除了作為更有效的計(jì)算之外,與掩碼表達(dá)式相比硫兰,這更容易閱讀和理解诅愚。注意query()
方法也接受@
標(biāo)志來標(biāo)記局部變量:
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
性能:什么時(shí)候使用這些函數(shù)
在考慮是否使用這些函數(shù)時(shí),有兩個(gè)注意事項(xiàng):計(jì)算時(shí)間和內(nèi)存使用劫映。內(nèi)存使用是最可預(yù)測(cè)的方面违孝。 如前所述,涉及 NumPy 數(shù)組或 Pandas DataFrame
的每個(gè)復(fù)合表達(dá)式泳赋,都會(huì)產(chǎn)生隱式創(chuàng)建的臨時(shí)數(shù)組:例如雌桑,這個(gè):
x = df[(df.A < 0.5) & (df.B < 0.5)]
大致相當(dāng)于這個(gè):
tmp1 = df.A < 0.5
tmp2 = df.B < 0.5
tmp3 = tmp1 & tmp2
x = df[tmp3]
如果臨時(shí)DataFrame
的大小與可用的系統(tǒng)內(nèi)存(通常是幾千兆字節(jié))相比很大,那么使用eval()
或query()
表達(dá)式是個(gè)好主意祖今。你可以使用以下方法檢查數(shù)組的大致大行?印(以字節(jié)為單位):
df.values.nbytes
# 32000
在性能方面拣技,即使你沒有超出你的系統(tǒng)內(nèi)存,eval()
也會(huì)更快耍目。問題是你的臨時(shí)DataFrame
與系統(tǒng)上的 L1 或 L2 CPU 緩存的大小相比(2016 年通常為幾兆字節(jié))如何膏斤;如果它們更大,那么eval()
可以避免不同內(nèi)存緩存之間的某些值移動(dòng)邪驮,它們可能很慢莫辨。
在實(shí)踐中,我發(fā)現(xiàn)傳統(tǒng)方法和eval/query
方法之間的計(jì)算時(shí)間差異毅访,通常不大 - 如果有的話沮榜,傳統(tǒng)方法對(duì)于較小的數(shù)組來說更快!eval/query
的好處主要在于節(jié)省的內(nèi)存喻粹,以及它們提供的有時(shí)更清晰的語法蟆融。
我們已經(jīng)涵蓋了eval()
和query()
的大部分細(xì)節(jié);對(duì)于這些的更多信息磷斧,你可以參考 Pandas 文檔振愿。特別是,可以指定執(zhí)行這些查詢的不同解析器和引擎弛饭;詳細(xì)信息請(qǐng)參閱“提升性能”部分中的討論冕末。