pandas(9)

《Python数据科学手册》读书笔记

高性能Pandas: eval()与query()

query()与eval()的设计动机: 复合代数式

NumPy 与 Pandas 都支持快速的向量化运算。 例如,
可以对下面两个数组进行求和:

import numpy as np
rng = np.random.RandomState(42)
x = rng.rand(1000000)
y = rng.rand(1000000)
%timeit x + y
4.01 ms ± 44.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

这样做比普通的 Python 循环或列表综合要快
很多:

%timeit np.fromiter((xi + yi for xi, yi in zip(x, y)), dtype=x.dtype, count=len(x))
563 ms ± 50.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

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

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

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

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

也就是说, 每段中间过程都需要显式地分配内存。 如果 x 数组和 y 数
组非常大, 这么运算就会占用大量的时间和内存消耗。

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:

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

%timeit df1 + df2 + df3 + df4
116 ms ± 2.23 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

也可以通过 pd.eval 和字符串代数式计算并得出相同的结果:

%timeit pd.eval('df1 + df2 + df3 + df4')
53.7 ms ± 1.36 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

这个 eval() 版本的代数式比普通方法快一倍(而且内存消耗更少) ,
结果也是一样的:

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() 支持所有的比较运算符, 包括链式代数式
(chained expression) :

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()实现列间运算

由于 pd.eval() 是 Pandas 的顶层函数, 因此 DataFrame 有一个
eval() 方法可以做类似的运算。 使用 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

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

  1. 用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
  1. 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 对象的
命名空间) 计算代数式。 需要注意的是, @ 符号只能在
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() 方法也支持用 @ 符号引用局部变量:

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 的复合代数式都会产生临时数组,
例如:

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]

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

 df.values.nbytes
32000

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

发布了57 篇原创文章 · 获赞 63 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/weixin_41503009/article/details/104185522
9
9*9