正如我们在前面的文章中已经介绍过的,Python 数据科学生态环境的强大力量是建立在 NumPy 和 Pandas 的基础之上,并通过直观的语法将基本操作转换成C语言:
在NumPy中,是向量化/广播式操作。
在Pandas中是分组式运算。
虽然这些抽象功能对于许多常见的用例来说是简洁和高效的,但它们往往依赖于临时中间对象的创建,这样就会占用大量的计算时间和内存,造成算力的不必要的浪费。
从0.13版本(2014年1月正式发布)开始,Pandas引入了一些实验性的工具,允许用户直接运行C语言速度的操作,而不需要费力地配置中间数组。这些就是eval() 和 query() 函数。
eval()和query()它们依赖于Numexpr程序包。
(https://github.com/pydata/numexpr)。
下面我们将介绍它们的使用方法,并给出我自己做项目中的一些经验,告诉你何时可以考虑使用它们。
# query() 和 eval() 的设计动机:复合表达式
前面我们已经看到,NumPy 和 Pandas 都支持快速的向量化操作;例如,对下面两个数组进行求和:
这样做比普通的Python 循环或列表综合要快很多:
但这种运算在处理复合表达式的问题时,效率会变得不那么高效。例如,考虑以下表达式:
因为 NumPy 会对每个代数子表达式进行计算,所以大致等价于以下内容:
换句话说,每一个中间步骤都在内存中显式分配。
如果x和y数组非常大,会导致大量的内存和计算开销。
Numexpr 程序库提供了元素到元素的复合代数式运算,而无需分配完整的中间数组。
Numexpr 文档中有更多的细节,但简而言之,这个程序库其实就是用一个 NumPy 风格的字符串代数式进行运算:
好处是,Numexpr 在计算代数式时不需要为临时数组分配全部内存,因此可以比 NumPy 更高效,尤其是对于大型数组的处理更为适合。
我们将在下面讨论 Pandas 的 eval() 和 query() 工具在概念上是相似的,并且也是基于于 Numexpr 来实现的。
# 用 pandas.eval() 实现高性能运算
Pandas中的 eval() 函数使用字符串表达式实现了 DataFrames 的高性能运算。例如以下的DataFrames:
如果要用普通的 Pandas 方法计算所有四个 DataFrames 的总和,我们可以直接写出总和:
可以通过 pd.eval 和字符串表达式计算出同样的结果:
这个表达式的 eval() 版本比普通方法大约快了一倍(而且使用的内存少得多),同时给出了同样的结果:
## pd.eval() 支持的运算
从Pandas v0.16版本开始,pd.eval()支持多种操作运算。为了演示这些操作,我们将使用一个整数类型的 DataFrames:
### 算术运算符
pd.eval() 支持所有的算术运算符。例如:
### 比较运算符
pd.eval() 支持所有比较运算符,包括链式表达式:
### 位运算符
pd.eval() 支持 & 和 | (或)等位运算符:
此外,它还支持在布尔表达式中使用 and 和 or 等字面值:
### 对象属性与索引
pd.eval() 支持通过 obj.attr 语法获取对象属性,以及通过 obj[index] 语法获取对象索引:
### 其他运算
其他的操作,如函数调用、条件语句、循环和其他更复杂的运算。目前还不能在 pd.eval() 中实现。
如果你想执行这些更复杂的表达式类型,你可以使用Numexpr 库来实现。
# 用 DataFrame.eval() 实现列间运算
就像 Pandas 有一个顶层的 pd.eval() 函数一样,DataFrames 也有一个 eval() 方法可以做类似的运算。
使用 eval() 方法的好处是可以借助列名称来进行运算。我们以这个标签数组为例:
如果使用前面介绍的 pd.eval(),我们可以通过下面的代数式计算出这三列:
DataFrame.eval() 方法可以通过列名称实现更简洁的表达式:
请注意,这里我们将列名称作为变量来计算表达式,结果也是正确的。
## 用DataFrame.eval() 新增列
除了刚才讨论的运算功能外,DataFrame.eval() 还允许创建新的列。
让我们使用之前的 DataFrame 来演示,它有 'A'、'B' 和 'C' 列:
我们可以使用 df.eval() 来创建一个新的列'D',并给它分配一个从其他列计算出来的值:
还可以修改已有的列:
## DataFrame.eval() 使用局部变量
DataFrame.eval() 方法支持一种额外的语法,它允许它与本地 Python 变量一起工作。如下所示:
这里的 @ 字符表示这是一个变量名,而不是一个列名,它让你有效地评估两个 "命名空间 "的表达式:列的命名空间和 Python 对象的命名空间。
注意这个 @ 字符只能在 DataFrame.eval() 方法中使用,而不能在 pandas.eval() 函数中使用,因为 pandas.eval()函数只能获取一个 (Python) 命名空间的内容。
# DataFrame.query() 方法
DataFrame有另一个基于已评估字符串的方法,称为query()方法。考虑以下内容:
就像在我们讨论中使用的例子一样DataFrame.eval(),这是一个涉及列的表达式DataFrame。
但是,不能使用DataFrame.eval()语法来表达它!
相反,对于这种类型的过滤操作,可以使用以下query()方法:
与掩蔽表达式相比,除了计算效率更高之外,它更易于阅读和理解。请注意,该query()方法还接受@标记以标记局部变量:
# 何时使用eval()和query()
性能决定使用时机。
在考虑是否使用这些功能时,有两个注意事项:计算时间和内存使用量。
内存使用是最可预测的方面。如前所述,每个涉及NumPy数组或PandasDataFrame的复合表达式都将导致隐式创建临时数组。例如:
大致相当于:
如果DataFrame临时占用内存比我们电脑可用系统内存要大,(通常为数GB),则最好使用eval() orquery()表达式。
我们可以使用以下方法检查数组的近似大小(以字节为单位):
在性能方面,eval()即使您没有最大限度地利用系统内存,也可以更快。
本文介绍了eval()和query()的使用方法。主要是让计算机的效率达到最高,是Pandas高级用法之一。
关注?君,持续学习精进。