一、说明
Python数据科学生态环境的强大力量在Numpy和Pandas的基础之上,并通过直观的语法将基本操作转化为c语言:在Numpy里是向量化/广播运算,在pandas里是分组型的运算。虽然这些抽象功能可以简洁高效的解决很多问题,但是他们经常需要创建临时对象,这样会占用很大的计算时间和内存。
Pandas为了解决性能问题,引入了eval()函数和query()函数,实现了直接运行C语言速度的操作,不需要费力配置中间数组,它们都依赖于Numexpr程序包。
import numpy as np
x = np.random.rand(1000000)
y = np.random.rand(1000000)
# numpy的向量化运算
%timeit x + y # timeit模块:准确测量小段代码的执行时间
# python的列表运算
%timeit np.fromiter([x1+y1 for x1, y1 in zip(x, y)], dtype=np.float)
输出结果
1.58 ms ± 60.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
177 ms ± 1.75 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
对于上面的numpy向量化运算,其优点很明显:对比普通的python循环或者列表综合运行速度要快很多,但是对于下面的复合代数式问题,numpy的向量化运算效率也比较低。
mask = (x>0.5) & (x<0.5)
#上式等价于于:
tmp1 = (x>0.5)
tmp2 = (y<0.5)
mask = tmp1 & tmp2
原因是,每段中间过程都需要显式的分配内存。如果x数组和y数组很大,这么运算将会占用大量的时间和内存。Numexpr程序库可以实现不为中间过程分配全部内存的前提下,完成元素到元素的复合代数式运算。Pandas的eval函数()和query()函数就是基于Numexpr实现的。
二、pandas.eval()函数
1. 算术运算
import numpy as np
df1, df2, df3, df4, df5 = (pd.DataFrame(rng.randint(0, 1000, (100, 3))) for i in range(5))
result1 = -df1 * df2 / (df3 + df4) - df5
result2 = pd.eval('-df1 * df2 / (df3 + df4) - df5')
np.allclose(result1, result2) # np.allclose():,比较两个array的每一元素是否相等
True
2. 比较运算
result1 = (df1 < df2) & (df2 <= df3) & (df3 != df4)
result2 = pd.eval('df1 < df2 <= df3 != df4')
np.allclose(result1, result2)
True
3. 位运算
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
4. 对象属性和索引
result1 = df2.T[0] + df3.iloc[1]
result2 = pd.eval('df2.T[0] + df3.iloc[1]')
np.allclose(result1, result2)
True
三、DataFrame.eval()
- pandas.eval() 是 Pandas 的顶层函数,因此 DataFrame 也有一个 eval()方法可以做类似的运算;
- DataFrame.eval()方法的好处:通过列名实现简洁的代数式运算。
1. 列名作为变量进行代数运算
df = pd.DataFrame(rng.rand(1000, 3), columns=['A', 'B', 'C'])
df.head()
A B C
0 0.401791 0.973228 0.005811
1 0.453365 0.715901 0.635402
2 0.171049 0.175610 0.004500
3 0.254660 0.513748 0.754389
4 0.897135 0.649130 0.368049
# 三种结果相同的不同写法,第3种 最高效简洁
result1 = (df['A'] + df['B']) / (df['C'] - 1)
result2 = pd.eval("(df.A + df.B) / (df.C - 1)")
result3 = df.eval('(A + B) / (C - 1)')
np.allclose(result1, result2)
True
np.allclose(result1, result3)
True
2. 新增列
df.eval('D = (A + B) / C', inplace=True) # 新增D列
df.head()
A B C D
0 0.401791 0.973228 0.005811 236.627079
1 0.453365 0.715901 0.635402 1.840199
2 0.171049 0.175610 0.004500 77.033882
3 0.254660 0.513748 0.754389 1.018584
4 0.897135 0.649130 0.368049 4.201252
3. 局部变量
- 通过
@
符号可以使用 Python 的局部变量; -
@
符号表示“这是一个变量名称而不是一个列名称”。从而灵活地使用两个“命名空间”的资源计算代数式(列名称的命名空间、Python 对象的命名空间); - 说明:该方法不能在 pandas.eval() 函数中使用,因为 pandas.eval() 函数只能获取一个命名空间的内容。
column_mean = df.mean(1)
result1 = df['A'] + column_mean
result2 = df.eval('A + @column_mean')
np.allclose(result1, result2)
True
四、DataFrame.query()
- query()函数和eval()函数一样,是基于DataFrame列计算代数式。通常,过滤操作时,使用query()函数更简洁;
result1 = pd.eval('df[(df.A < 0.5) & (df.B < 0.5)]')
result2 = df.query('A < 0.5 and B < 0.5')
np.allclose(result1, result2)
True
- query()函数也支持局部变量,同样是通过关键符
@
进行识别,当存在isin()
判断时,需要使用==
代替isin()
。特别地,因为pandas本身没有isnotin()函数,如果需要按此逻辑进行判断,仅需要把==
改为!=
即可。
filter_list = [2, 6, 10] # 按列表元素筛选
df = pd.DataFrame({"A": [5, 0, 1, 2, 4, 3], "B": [2, 7, 8, 9, 6, 10]})
# isin()判断
df.query("B == @ filter_list")
A B
0 5 2
4 4 6
5 3 10
# is not in 判断
df.query("B != @ filter_list")
A B
1 0 7
2 1 8
3 2 9
五、性能
- 在考虑要不要用这两个函数时,需要思考两个方面:计算时间和内存消耗,而内存消耗是更重要的影响因素;
- 每个涉及 NumPy 数组或 Pandas 的 DataFrame的复合代数式运算时,都会产生临时数组;
- 普通方法在处理较小的数组时,反而速度更快! eval()函数和query 函数的优点主要是节省内存,语法也更加简洁。