# 7.15 高性能 Pandas：`eval()`和`query()`

> 原文：[High-Performance Pandas: `eval()` and `query()`](https://nbviewer.jupyter.org/github/donnemartin/data-science-ipython-notebooks/blob/master/pandas/03.12-Performance-Eval-and-Query.ipynb)
> 
> 译者：[飞龙](https://github.com/wizardforcel)
> 
> 协议：[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)
> 
> 本节是[《Python 数据科学手册》](https://github.com/jakevdp/PythonDataScienceHandbook)（Python Data Science Handbook）的摘录。

我们在前面的章节中已经看到，PyData 技术栈的力量，建立在 NumPy 和 Pandas 通过直观语法，将基本操作推送到 C 的能力的基础上：例如 NumPy 中的向量化/广播操作，以及 Pandas 的分组类型操作。虽然这些抽象对于许多常见用例是高效且有效的，但它们通常依赖于临时中间对象的创建，这可能产生计算时间和内存使用的开销。

从版本 0.13（2014 年 1 月发布）开始，Pandas 包含一些实验性工具，允许你直接访问速度和 C 一样的操作，而无需昂贵的中间数组分配。这些是``eval()``和``query()``函数，它依赖于 [Numexpr](https://github.com/pydata/numexpr) 包。在这个笔记本中，我们将逐步介绍它们的使用方法，并提供一些何时可以考虑使用它们的经验法则。

## ``query()``和``eval()``的动机：复合表达式

我们以前见过 NumPy 和 Pandas 支持快速向量化操作；例如，相加两个数组的元素时：

```py
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 数组的计算：通用函数”中所讨论的，这比通过 Python 循环或推导式执行加法要快得多：

```py
%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
```

但是在计算复合表达式时，这种抽象可能变得不那么有效。例如，请考虑以下表达式：

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

因为 NumPy 会计算每个子表达式，所以大致相当于以下内容：

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

换句话说，每个中间步骤都在内存中明确分配。如果``x``和``y``数组非常大，这可能会产生大量内存和计算开销。Numexpr 库使你能够逐元素计算这种类型的复合表达式，而无需分配完整的中间数组。[Numexpr 文档](https://github.com/pydata/numexpr)有更多细节，但暂时可以说，这个库接受字符串，它提供了你想要计算的 NumPy 风格的表达式：

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

# True
```

这里的好处是，Numexpr 以不使用完整临时数组的方式计算表达式，因此可以比 NumPy 更有效，特别是对于大型数组。我们将在这里讨论的 Pandas ``eval()``和``query()``工具，在概念上是相似的，并且依赖于 Numexpr 包。

## 用于高效操作的``pandas.eval()``

Pandas 中的``eval()``函数接受字符串表达式，来使用``DataFrame``高效地计算操作。例如，考虑以下``DataFrame``：

```py
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 方法计算所有四个`DataFrame`的和，我们可以写出总和：

```py
%timeit df1 + df2 + df3 + df4

# 10 loops, best of 3: 87.1 ms per loop
```

通过将表达式构造为字符串，可以通过``pd.eval``计算相同的结果：

```py
%timeit pd.eval('df1 + df2 + df3 + df4')

# 10 loops, best of 3: 42.2 ms per loop
```

这个表达式的``eval()``版本速度提高了约 50%（并且使用的内存更少），同时给出了相同的结果：

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

### ``pd.eval()``所支持的操作

从 Pandas v0.16 开始，``pd.eval()``支持广泛的操作。为了演示这些，我们将使用以下整数``DataFrame``：

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

#### 算术运算符

``pd.eval()``支持所有算术运算符，例如：

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

# True
```

#### 比较运算符

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

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

# True
```

#### 按位运算符

``pd.eval()``支持``&``和``|``按位运算符：

```py
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`：

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

# True
```

#### 对象属性和索引

``pd.eval()``支持通过``obj.attr``语法访问对象属性，和通过``obj[index]``语法进行索引：

```py
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()``方法，它的工作方式类似。``eval()``方法的好处是列可以通过名称引用。我们将使用这个带标签的数组作为示例：

```py
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()``，我们可以像这样使用三列来计算表达式：

```py
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()``方法允许使用列来更简洁地求解表达式：

```py
result3 = df.eval('(A + B) / (C - 1)')
np.allclose(result1, result3)

# True
```

请注意，我们将列名称视为要求解的表达式中的变量，结果是我们希望的结果。

### `DataFrame.eval()`中的赋值

除了刚才讨论的选项之外，``DataFrame.eval()``还允许赋值给任何列。让我们使用之前的``DataFrame``，它有列`A`，`B`和`C`：

```py
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'``并为其赋一个从其他列计算的值：

```py
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 |

以同样的方式，可以修改任何现有列：

```py
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 局部变量。考虑以下：

```py
column_mean = df.mean(1)
result1 = df['A'] + column_mean
result2 = df.eval('A + @column_mean')
np.allclose(result1, result2)

# True
```

这里的`@`字符标记变量名而不是列名，并允许你高效计算涉及两个“名称空间”的表达式：列的名称空间和 Python 对象的名称空间。请注意，这个`@`字符仅由``DataFrame.eval()``方法支持，不由``pandas.eval()``函数支持，因为``pandas.eval ()``函数只能访问一个（Python）命名空间。

## `DataFrame.query()`方法

``DataFrame``有另一种基于字符串的求值方法，称为``query()``方法。考虑以下：

```py
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()``方法：

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

# True
```

除了作为更有效的计算之外，与掩码表达式相比，这更容易阅读和理解。注意``query()``方法也接受``@``标志来标记局部变量：

```py
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 `DataFrame`的每个复合表达式，都会产生隐式创建的临时数组：例如，这个：

```py
x = df[(df.A < 0.5) & (df.B < 0.5)]
```

大致相当于这个：

```py
tmp1 = df.A < 0.5
tmp2 = df.B < 0.5
tmp3 = tmp1 & tmp2
x = df[tmp3]
```

如果临时`DataFrame`的大小与可用的系统内存（通常是几千兆字节）相比很大，那么使用``eval()``或``query()``表达式是个好主意。你可以使用以下方法检查数组的大致大小（以字节为单位）：

```py
df.values.nbytes

# 32000
```

在性能方面，即使你没有超出你的系统内存，``eval()``也会更快。问题是你的临时`DataFrame`与系统上的 L1 或 L2 CPU 缓存的大小相比（2016 年通常为几兆字节）如何；如果它们更大，那么``eval()``可以避免不同内存缓存之间的某些值移动，它们可能很慢。

在实践中，我发现传统方法和``eval/query``方法之间的计算时间差异，通常不大 - 如果有的话，传统方法对于较小的数组来说更快！``eval/query``的好处主要在于节省的内存，以及它们提供的有时更清晰的语法。

我们已经涵盖了``eval()``和``query()``的大部分细节；对于这些的更多信息，你可以参考 Pandas 文档。特别是，可以指定执行这些查询的不同解析器和引擎；详细信息请参阅“提升性能”部分中的讨论。
