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.里面的讨论。

推荐阅读更多精彩内容