12.高性能Pandas:eval和query

正如我们在前面几节中已经看到的,PyData堆栈的强大功能建立在NumPy和Pandas通过直观语法将基本操作使用C实现能力之上:例如NumPy的矢量化/广播操作,Pandas的分组类型操作。尽管这些抽象概念在许多常见的情况是有效和起作用的,它们经常依赖创建临时中间对象,它们导致计算时间和内存使用的不当开销。
从版本0.13开始,Pandas包含了一下实验性的工具允许你直接使用C速度操作,避免中间数组的浪费。这些工具是eval()和 query()函数,它们依赖 Numexpr包。我们将浏览它们的用法,并给出一些关于何时使用它们的经验法则。

eval()和 query()动机:复合表达式

我们已经看到NumPy和Pandas支持快速的矢量化操作;如,计算两个数组元素的和值:

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

如我们在Computation on NumPy Arrays: Universal Functions讨论的,这比通过使用Python 循环和解析做加法要快许多:

%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

但是这种概念在计算复合表达式时效率就不高了。例如,考虑如下表达:

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

因为NumPy评估每个子表达式,这基本相当于做如下操作:

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

换句话说,每个步骤都需要明确的分配内存。如果X和y数组很大的话,那将导致显著的内存和计算开销。Numexpr库按元素计算这种复合表达式的能力,并不用分配完整的中间数组。文档The Numexpr documentation 由许多细节,但就目前来说,理解这个库接受想要计算的NumPy样式表达式的字符串就足够了:

import numexpr
mask_numexpr = numexpr.evaluate('(x > 0.5) & (y < 0.5)')
np.allclose(mask, mask_numexpr)
True

Numexpr计算表达式的好处是它不必使用全部临时数组,并且对于大数组来说,比NumPy效率更高。我们在这里讨论的Panda seval()和query()工具概念上类似,并且依赖于Numexpr包。

pandas.eval()用于高效操作

Pandas的evaluate()函数使用字符串表达式来来计算对DataFrame的操作。例如,考虑如下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))

为了计算四个DATaFrame的和,使用典型Pandas方法,我们可以这样实现:

%timeit df1 + df2 + df3 + df4
10 loops, best of 3: 87.1 ms per loop

通过构建表达式字符串,使用pd.eval计算同样结果:

%timeit pd.eval('df1 + df2 + df3 + df4')
10 loops, best of 3: 42.2 ms per loop

eval()表达式版本速度快50%(并且更省内存):

np.allclose(df1 + df2 + df3 + df4,
            pd.eval('df1 + df2 + df3 + df4'))
True

pd.eval()支持的操作

在Pandas v0.16,pd.eval()支持许多操作。为演示它们,我们将使用如下整型DataFrame:

df1, df2, df3, df4, df5 = (pd.DataFrame(rng.randint(0, 1000, (100, 3)))
                           for i in range(5))

算术运算符

pd.eval()支持所有算术运算符。例如:

result1 = -df1 * df2 / (df3 + df4) - df5
result2 = pd.eval('-df1 * df2 / (df3 + df4) - df5')
np.allclose(result1, result2)
True

比较运算符

pd.eval() 支持所有比较运算符,包括链式表达:

result1 = (df1 < df2) & (df2 <= df3) & (df3 != df4)
result2 = pd.eval('df1 < df2 <= df3 != df4')
np.allclose(result1, result2)
True

位操作符

pd.eval() 支持&和 |位操作符

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

另外,他支持在布尔表达式中使用文本的 and和or

result3 = pd.eval('(df1 < 0.5) and (df2 < 0.5) or (df3 < df4)')
np.allclose(result1, result3)
True

对象属性和索引

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

其它操作

pd.eval()并没有支持诸如函数调用,条件声明,循环,等其它复杂构造。如果想要执行内置复杂类型的表达式,可以自己使用Numexpr 库

DataFrame.eval()用于按列操作

正如Pandas由顶层的pd.eval()函数,DataFrame也有同样发生工作的eval()方法。DataFrame.eval()方法的好处是它可以按名称指定列。我们使用这个标记数组作为例子:

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(),我们可以这样使用三列的表达式:

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

这里注意我们在求值表达式将列名看做变量,并且结果是我们希望的。

DataFrame.eval()中赋值

除了之前讨论的选项,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()来创建一个新列D,并且将从其它列计算出的值赋给它:

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

同样方式,任何已有的列也可以被修改:

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

@字符这里标记是一个变量名称而不是一个列名,并且让你高效的计算两个名字空间(列内部和Python对象)的表达式。注意zhi只是 DataFrame.eval()方法支持@字符,pandas.eval()函数并不支持它,因为pandas.eval()函数只访问一个(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()的例子,这是一个包含DataFrame列的表达式。但是无法使用DataFrame.eval()语法表示。相反在这类的过滤操作中,你可以使用query()方法:

result2 = df.query('A < 0.5 and B < 0.5')
np.allclose(result1, result2)
True

除了计算效率更高,与过滤表达式相比query方法更简洁更易懂。注意query()方法也接受@符合来标记本地变量:

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 DataFrames的复合表达式都会导致隐式创建临时数组:例如:

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]

如果临时DataFrames的大小相对于你系统可用内存来说很显著,那么使用eval()或query()表达式就是个好主意。你可用使用如下方法检查数据的大概大小:

df.values.nbytes
32000

在性能方面,eval()即使在系统内存没有最优化的情况下,运行的也快得多。速度取决于数据临时DataFrame大小与系统中L1或L2cpu 缓存大小相比结果;如果临时数据更大,那么eval()更快,因为它能避免潜在的数据在不同内存缓存间的移动。在实际中,我发现传统方法和eval/query方法的运行时间不没有显著不同--如果有的话,传统方法在小数组时运行得更快。eval/query得好处主要时节省内存,以及有时候简洁得语法。
我们这里已经涵盖了eval()和query()大部分细节;更多信息,请参考Pandas文档。另外,可用指定不同得解析器和引擎来运行这些查询;关于这部分的细节,参见"Enhancing Performance" section.里面的讨论。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容