數(shù)據(jù)科學(xué) IPython 筆記本 7.15 高性能 Pandas:`eval()`和`query()`

# 7.15 高性能 Pandas:eval()query()

原文:High-Performance Pandas: eval() and query()

譯者:飛龍

協(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)存中明確分配。如果xy數(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á)式中使用字面andor

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ù)一樣嵌器,DataFrameeval()方法,它的工作方式類似谐丢。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趟卸,它有列ABC

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)參閱“提升性能”部分中的討論冕末。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市侣颂,隨后出現(xiàn)的幾起案子档桃,更是在濱河造成了極大的恐慌,老刑警劉巖憔晒,帶你破解...
    沈念sama閱讀 221,695評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件藻肄,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡拒担,警方通過查閱死者的電腦和手機(jī)嘹屯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,569評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來从撼,“玉大人州弟,你說我怎么就攤上這事〉土悖” “怎么了婆翔?”我有些...
    開封第一講書人閱讀 168,130評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)掏婶。 經(jīng)常有香客問我啃奴,道長(zhǎng),這世上最難降的妖魔是什么雄妥? 我笑而不...
    開封第一講書人閱讀 59,648評(píng)論 1 297
  • 正文 為了忘掉前任最蕾,我火速辦了婚禮依溯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘揖膜。我一直安慰自己誓沸,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,655評(píng)論 6 397
  • 文/花漫 我一把揭開白布壹粟。 她就那樣靜靜地躺著拜隧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪趁仙。 梳的紋絲不亂的頭發(fā)上洪添,一...
    開封第一講書人閱讀 52,268評(píng)論 1 309
  • 那天,我揣著相機(jī)與錄音雀费,去河邊找鬼干奢。 笑死,一個(gè)胖子當(dāng)著我的面吹牛盏袄,可吹牛的內(nèi)容都是我干的忿峻。 我是一名探鬼主播,決...
    沈念sama閱讀 40,835評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼辕羽,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼逛尚!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起刁愿,我...
    開封第一講書人閱讀 39,740評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤绰寞,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后铣口,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體滤钱,經(jīng)...
    沈念sama閱讀 46,286評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,375評(píng)論 3 340
  • 正文 我和宋清朗相戀三年脑题,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了件缸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,505評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡叔遂,死狀恐怖他炊,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情掏熬,我是刑警寧澤佑稠,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布秒梅,位于F島的核電站旗芬,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏捆蜀。R本人自食惡果不足惜疮丛,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,873評(píng)論 3 333
  • 文/蒙蒙 一幔嫂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧誊薄,春花似錦履恩、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,357評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至片吊,卻和暖如春绽昏,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背俏脊。 一陣腳步聲響...
    開封第一講書人閱讀 33,466評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工全谤, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人爷贫。 一個(gè)月前我還...
    沈念sama閱讀 48,921評(píng)論 3 376
  • 正文 我出身青樓认然,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親漫萄。 傳聞我的和親對(duì)象是個(gè)殘疾皇子卷员,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,515評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容