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