pandas高阶函数eval()和query()

https://www.jianshu.com/p/ac45e8d168ea

一、说明

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(np.random.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(np.random.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()

  1. 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
  1. 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 函数的优点主要是节省内存,语法也更加简洁。


作者:惑也
链接:https://www.jianshu.com/p/ac45e8d168ea
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
posted @ 2021-03-10 00:11  正在学Python  阅读(775)  评论(0编辑  收藏  举报