正如我们在前面几节中已经看到的,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.里面的讨论。