[Python3] Pandas v1.0 —— (九) 高性能Pandas: eval()与query()


[ Pandas version: 1.0.1 ]


十二、高性能Pandas:eval()与query()

Pandas数据科学生态环境的强大力量建立在NumPy与Pandas的基础上,并通过直观的语法将基本操作转换成C语言:在NumPy里是向量化/广播运算,在Pandas里是分组型的运算。

虽然这些抽象功能可以简洁高效地解决许多问题,但是它们经常需要创建临时中间对象,这样就会占用大量的计算时间与内存。

Pandas从0.13版本开始就引入了实验性工具,让用户可以直接运行C语言速度的操作,不需要十分费力地配置中间数组。它们就是eval()query()函数,都依赖于Numexpr程序包。

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

NumPy和Pandas快速向量化运算比普通Python循环或列表综合要快很多:

# 对两个数组进行求和
import numpy as np
rng = np.random.RandomState(42)
x = rng.rand(int(1E6))
y = rng.rand(int(1E6))
%timeit x + y
# 3.54 ms ± 172 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

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

但这种运算在处理复合代数式(compound expression)问题时效率比较低:每段中间过程都需要显式地分配内存

如果x数组和y数组非常大,运算就会占用大量的时间和内存消耗。

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

# 由于NumPy会计算每一个代数子式,计算过程等价于:
# tmp1 = (x > 0.5)
# tmp2 = (y < 0.5)
# mask = tmp1 & tmp2

Numexpr程序包可以在不为中间过程分配全部内存的前提下,完成元素到元素的复合代数式运算(用一个NumPy风格的字符串代数式进行运算)

  • 优点:Numexpr在计算代数式时不需要为临时数组分配全部内存,因此计算比NumPy更高效,尤其适用处理大型数组
  • Pandas的eval()query()工具也是基于Numexpr实现的
import numexpr
mask_numexpr = numexpr.evaluate('(x > 0.5) & (y < 0.5)')
np.allclose(mask, mask_numexpr)
# True

(二)用pandas.eval()实现高性能运算

Pandas的eval()函数用字符串代数式实现了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
# 93.4 ms ± 8.72 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

# pd.eval和字符串代数式计算并得出相同结果(比普通方法快一倍且内存消耗更少,结果也相同)
%timeit pd.eval('df1 + df2 + df3 + df4')
# 49.4 ms ± 3.82 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)

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

pd.eval()支持的运算

# 创建一个整数类型的DataFrame
df1, df2, df3, df4, df5 = (pd.DataFrame(rng.randint(0, 100, (100, 3))) for i in range(5))
(1) 算术运算符

pd.eval()支持所有的算术运算符

result1 = -df1 * df2 / (df3 + df4) - df5
result2 = pd.eval('-df1 * df2 / (df3 + df4) - df5')
np.allclose(result1, result2)
# True
(2) 比较运算符

pd.eval()支持所有的比较运算符+, -, *, /, **, %, //,包括链式代数式(chained expression)

result1 = (df1 < df2) & (df2 <= df3) & (df3 != df4)
result2 = pd.eval('df1 < df2 <= df3 != df4')
np.allclose(result1, result2)
# True
(3) 位运算符

pd.eval()支持| (or), & (and), ~ (not)等位运算符,另外还可以在布尔类型的代数式中使用andor等字面值

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

result3 = pd.eval('(df1 < 0.5) and (df2 < 0.5) or (df3 < df4)')
np.allclose(result1, result3)
# True
(4) 对象属性与索引

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
(5) 其他运算

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.374540  0.950714  0.731994
# 1  0.598658  0.156019  0.155995
# 2  0.058084  0.866176  0.601115
# 3  0.708073  0.020584  0.969910
# 4  0.832443  0.212339  0.181825

# 用od.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()还可以创建新的列。

df.head()
#           A         B         C
# 0  0.374540  0.950714  0.731994
# 1  0.598658  0.156019  0.155995
# 2  0.058084  0.866176  0.601115
# 3  0.708073  0.020584  0.969910
# 4  0.832443  0.212339  0.181825

# 可以用df.eval()创建一个新列'D'并赋给它其他列计算的值
df.eval('D = (A + B) / C', inplace=True)
df.head()
#           A         B         C         D
# 0  0.374540  0.950714  0.731994  1.810472
# 1  0.598658  0.156019  0.155995  4.837844
# 2  0.058084  0.866176  0.601115  1.537576
# 3  0.708073  0.020584  0.969910  0.751263
# 4  0.832443  0.212339  0.181825  5.746085

# 可以修改已有的列
df.eval('D = (A - B) / C', inplace=True)
df.head()
#           A         B         C         D
# 0  0.374540  0.950714  0.731994 -0.787130
# 1  0.598658  0.156019  0.155995  2.837535
# 2  0.058084  0.866176  0.601115 -1.344323
# 3  0.708073  0.020584  0.969910  0.708816
# 4  0.832443  0.212339  0.181825  3.410442

2. DataFrame.eval()使用局部变量

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

  • @符号表示“这是一个变量名称而不是一个列名称”,从而灵活用两个“命名空间”的资源(列名称的命名空间和Python对象的命名空间)计算代数式
  • 注意:@符号只能在DataFrame.eval()方法中使用,而不能在pd.eval()函数中使用,因为pd.eval()函数只能获取一个Python命名空间的内容
column_mean = df.mean(1)
result1 = df['A'] + column_mean
result2 = df.eval('A + @column_mean')
np.allclose(result1, result2)
# True

(四)DataFrame.query()方法

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

  • 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列创建的代数式,但不能用`DataFrame.eval()`语法
# 对于这种过滤运算可以用query()方法

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

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方法的优点主要是节省内存,有时语法也更加简洁。


Pandas 相关阅读:

[Python3] Pandas v1.0 —— (一) 对象、数据取值与运算
[Python3] Pandas v1.0 —— (二) 处理缺失值
[Python3] Pandas v1.0 —— (三) 层级索引
[Python3] Pandas v1.0 —— (四) 合并数据集
[Python3] Pandas v1.0 —— (五) 累计与分组
[Python3] Pandas v1.0 —— (六) 数据透视表
[Python3] Pandas v1.0 —— (七) 向量化字符串操作
[Python3] Pandas v1.0 —— (八) 处理时间序列
[Python3] Pandas v1.0 —— (九) 高性能Pandas: eval()与query() 【本文】


总结自《Python数据科学手册》

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页