13.6 Pandas高性能计算

前面的章节已经介绍过,Python 数据科学生态环境的强大力量建立在 NumPy 与 Pandas 的 基础之上,并通过直观的语法将基本操作转换成 C 语言:在 NumPy 里是向量化 / 广播运 算,在 Pandas 里是分组型的运算。虽然这些抽象功能可以简洁高效地解决许多问题,但是 它们经常需要创建临时中间对象,这样就会占用大量的计算时间与内存。

Pandas 从 0.13 版开始(2014 年 1 月)就引入了实验性工具,让用户可以直接运行 C 语言 速度的操作,不需要十分费力地配置中间数组。它们就是 eval() 和 query() 函数,都依赖 于 Numexpr (https://github.com/pydata/numexpr) 程序包。我们将在下面的 Notebook 中演示 其用法,并介绍一些使用时的注意事项。

14.6.1 Pandas高性能计算概述

前面已经介绍过,NumPy 与 Pandas 都支持快速的向量化运算。例如,你可以对下面两个

数组进行求和:

In[1]: import numpy as np

rng = np.random.RandomState(42)

x = rng.rand(1E6)

y = rng.rand(1E6)

%timeit x + y

100 loops, best of 3: 3.39 ms per loop

就像在 2.3 节介绍的那样,这样做比普通的 Python 循环或列表综合要快很多: In[2]:

%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

但是这种运算在处理复合代数式(compound expression)问题时的效率比较低,例如下面 的表达式:

In[3]: mask = (x > 0.5) & (y < 0.5)

由于 NumPy 会计算每一个代数子式,因此这个计算过程等价于:

In[4]: tmp1 = (x > 0.5)

tmp2 = (y < 0.5)

mask = tmp1 & tmp2

也就是说,每段中间过程都需要显式地分配内存。如果 x 数组和 y 数组非常大,这么运算 就会占用大量的时间和内存消耗。Numexpr 程序库可以让你在不为中间过程分配全部内 存的前提下,完成元素到元素的复合代数式运算。虽然 Numexpr 文档(https://github.com/ pydata/numexpr)里提供了更详细的内容,但是简单点儿说,这个程序库其实就是用一个 NumPy 风格的字符串代数式进行运算:

In[5]: import numexpr

mask_numexpr = numexpr.evaluate('(x > 0.5) & (y < 0.5)')

np.allclose(mask, mask_numexpr)

Out[5]: True

这么做的好处是,由于 Numexpr 在计算代数式时不需要为临时数组分配全部内存,因此计 算比 NumPy 更高效,尤其适合处理大型数组。马上要介绍的 Pandas 的 eval() 和 query() 工具其实也是基于 Numexpr 实现的。

14.6.2 eval()

Pandas 的 eval() 函数用字符串代数式实现了 DataFrame 的高性能运算,例如下面的

DataFrame:

In[6]: 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 的和,可以这么写:

In[7]: %timeit df1 + df2 + df3 + df4

10 loops, best of 3: 87.1 ms per loop 也可以通过 pd.eval 和字符串代数式计算并得出相同的结果:

In[8]: %timeit pd.eval('df1 + df2 + df3 + df4')

10 loops, best of 3: 42.2 ms per loop

这个 eval() 版本的代数式比普通方法快一倍(而且内存消耗更少),结果也是一样的: In[9]: np.allclose(df1 + df2 + df3 + df4,

pd.eval('df1 + df2 + df3 + df4'))

Out[9]: True

pd.eval()支持的运算 从 Pandas v0.16 版开始,pd.eval() 就支持许多运算了。为了演示这些运算,创建一个整数 类型的 DataFrame:

In[10]: df1, df2, df3, df4, df5 = (pd.DataFrame(rng.randint(0, 1000, (100, 3)))

for i in range(5))

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

In[11]: result1 = -df1 * df2 / (df3 + df4) - df5

result2 = pd.eval('-df1 * df2 / (df3 + df4) - df5')

np.allclose(result1, result2)

Out[11]: True (2) 比较运算符。pd.eval() 支持所有的比较运算符,包括链式代数式(chained expression):

In[12]: result1 = (df1 < df2) & (df2 <= df3) & (df3 != df4)

result2 = pd.eval('df1 < df2 <= df3 != df4')

np.allclose(result1, result2)

Out[12]: True (3) 位运算符。pd.eval() 支持 &(与)和 |(或)等位运算符:

In[13]: result1 = (df1 < 0.5) & (df2 < 0.5) | (df3 < df4)

result2 = pd.eval('(df1 < 0.5) & (df2 < 0.5) | (df3 < df4)')

np.allclose(result1, result2)

Out[13]: True 另外,你还可以在布尔类型的代数式中使用 and 和 or 等字面值:

In[14]: result3 = pd.eval('(df1 < 0.5) and (df2 < 0.5) or (df3 < df4)')

np.allclose(result1, result3)

Out[14]: True (4) 对象属性与索引。pd.eval() 可以通过 obj.attr 语法获取对象属性,通过 obj[index] 语

法获取对象索引:

In[15]: result1 = df2.T[0] + df3.iloc[1]

result2 = pd.eval('df2.T[0] + df3.iloc[1]')

np.allclose(result1, result2)

Out[15]: True (5) 其他运算。目前 pd.eval() 还不支持函数调用、条件语句、循环以及更复杂的运算。如

果你想要进行这些运算,可以借助 Numexpr 来实现。

14.6.3 query()

由于 pd.eval() 是 Pandas 的顶层函数,因此 DataFrame 有一个 eval() 方法可以做类似的运

算。使用 eval() 方法的好处是可以借助列名称进行运算,示例如下: In[16]: df = pd.DataFrame(rng.rand(1000, 3), columns=['A', 'B', 'C'])

df.head()

Out[16]:

In[19]: df.head()

Out[19]:

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(),就可以通过下面的代数式计算这三列:

In[17]: result1 = (df['A'] + df['B']) / (df['C'] - 1)

result2 = pd.eval("(df.A + df.B) / (df.C - 1)")

np.allclose(result1, result2)

Out[17]: True 而 DataFrame.eval() 方法可以通过列名称实现简洁的代数式:

In[18]: result3 = df.eval('(A + B) / (C - 1)')

np.allclose(result1, result3)

Out[18]: True

请注意,这里用列名称作为变量来计算代数式,结果同样是正确的。

1. 用DataFrame.eval()新增列 除了前面介绍的运算功能,DataFrame.eval() 还可以创建新的列。还用前面的 DataFrame 来演示,列名是 'A'、'B' 和 'C':

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',然后赋给它其他列计算的值: In[20]: df.eval('D = (A + B) / C', inplace=True)

df.head()

Out[20]: 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

还可以修改已有的列:

In[21]: df.eval('D = (A - B) / C', inplace=True)

df.head()

Out[21]: 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

2. DataFrame.eval()使用局部变量 DataFrame.eval() 方法还支持通过 @ 符号使用 Python 的局部变量,如下所示:

In[22]: column_mean = df.mean(1)

result1 = df['A'] + column_mean

result2 = df.eval('A + @column_mean')

np.allclose(result1, result2)

Out[22]: True

@ 符号表示“这是一个变量名称而不是一个列名称”,从而让你灵活地用两个“命名空 间”的资源(列名称的命名空间和 Python 对象的命名空间)计算代数式。需要注意的 是,@ 符号只能在 DataFrame.eval() 方法中使用,而不能在 pandas.eval() 函数中使用, 因为 pandas.eval() 函数只能获取一个(Python)命名空间的内容。

3.13.4 DataFrame.query()方法 DataFrame 基于字符串代数式的运算实现了另一个方法,被称为 query(),例如:

In[23]: 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)

Out[23]: True 和前面介绍过的 DataFrame.eval() 一样,这是一个用 DataFrame 列创建的代数式,但是不

能用 DataFrame.eval() 语法 5。不过,对于这种过滤运算,你可以用 query() 方法: In[24]: result2 = df.query('A < 0.5 and B < 0.5')

np.allclose(result1, result2)

Out[24]: True

除了计算性能更优之外,这种方法的语法也比掩码代数式语法更好理解。需要注意的是, query() 方法也支持用 @ 符号引用局部变量:

In[25]: Cmean = df['C'].mean()

result1 = df[(df.A < Cmean) & (df.B < Cmean)]

result2 = df.query('A < @Cmean and B < @Cmean')

注 5:因为你要的结果是包含 DataFrame 的全部列。——译者注 188 | 第3章

np.allclose(result1, result2)

Out[25]: True

14.5.4 高性能方法使用

在考虑要不要用这两个函数时,需要思考两个方面:计算时间和内存消耗,而内存消耗是 更重要的影响因素。就像前面介绍的那样,每个涉及 NumPy 数组或 Pandas 的 DataFrame 的复合代数式都会产生临时数组,例如:

In[26]: x = df[(df.A < 0.5) & (df.B < 0.5)]

它基本等价于:

In[27]: tmp1 = df.A < 0.5

tmp2 = df.B < 0.5

tmp3 = tmp1 & tmp2

x = df[tmp3]

如果临时 DataFrame 的内存需求比你的系统内存还大(通常是几吉字节),那么最好还是使 用 eval() 和 query() 代数式。你可以通过下面的方法大概估算一下变量的内存消耗:

In[28]: df.values.nbytes

Out[28]: 32000

在性能方面,即使你没有使用最大的系统内存,eval() 的计算速度也比普通方法快。现在的 性能瓶颈变成了临时 DataFrame 与系统 CPU 的 L1 和 L2 缓存(在 2016 年依然是几兆字节) 之间的对比了——如果系统缓存足够大,那么 eval() 就可以避免在不同缓存间缓慢地移动 临时文件。在实际工作中,我发现普通的计算方法与 eval/ query 计算方法在计算时间上的差 异并非总是那么明显,普通方法在处理较小的数组时反而速度更快! eval/ query 方法的优点 主要是节省内存,有时语法也更加简洁。

我们已经介绍了 eval() 与 query() 的绝大多数细节,若想了解更多的信息,请参考 Pandas 文档。尤其需要注意的是,可以通过设置不同的解析器和引擎来执行这些查询,相关细节 请 参 考 Pandas 文 档 中“Enhancing Performance”(http://pandas.pydata.org/pandas-docs/dev/ enhancingperf.html)节。

Last updated