Pandas的高性能计算:eval()与query()

Numpy与Pandas的底层实际上都是用C语言写成的并为Python预留了接口.

Numpy强大的能力来源于向量化运算和广播功能,而Pandas的强大能力来源于分组型运算

这些抽象的规则在赋予这两大工具强大的处理能力时,却也造成了一个困难:在进行处理时Pandas与Numpy经常会创建临时的中间对象

因此,Pandas从0.13版本(2014年1月发布)开始引入了实验性工具,允许用户直接以C语言速度的来运行程序,并且不需要费力的配置中间数组

即eval()和query()函数,他们都位于Numexpr库中


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

前面已经介绍过,Pandas和Numpy都支持快速的向量化运算

但是Pandas与Numpy支持的向量化运算的前提就是为所有参与运算的对象显式的分配内存

例如

import numpy

x=np.random.randint(0,10000,10000)
y=np.random.randint(0,10000,10000)
mask=(x > 500) & (y < 500)

Numpy处理上面的程序的步骤是:

  1. 为x和y分配内存并且生成随机数组
  2. 分配中间数组temp_1储存x>500的结果
  3. 分配中间数组temp_2储存y<500的结果
  4. 为mask分配内存并储存temp_1与temp_2与运算的结果

上面的过程一共为x,y,temp_1,temp_2,mask共五个数组分配了大小,最终一共划分了50000个单元内存出去

这在处理大型数据的时候非常的糟糕,所以向量化运算反而会造成程序处理复合代数式时效率的降低

这也就是Numexpr库创立的动机,就是去弥补Numpy的向量化运算在处理复合代数式时的缺陷

Numexpr的思路就是不为中间数组分配内存,即直接取x与y对应位置上的元素进行运算后直接储存到mask中

这样仅需要分配三个数组即可

下面将介绍的Pandas的eval()与query()都是基于Numexpr库实现的

Pandas.eval()实现高性能运算

Pandas的eval()函数用字符串代数式来实现了DataFrame的高性能运算

这里我们使用time模块的clock()函数来计算Python语句的CPU占用时间

DataFrame_1=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_2=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_3=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_4=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))

start_1=time.clock()
DataFrame_1+DataFrame_2+DataFrame_3+DataFrame_4
end_1=time.clock()
print(end_1-start_1)

start_2=time.clock()
pd.eval('DataFrame_1+DataFrame_2+DataFrame_3+DataFrame_4')
end_2=time.clock()
print(end_2-start_2)
>>>
0.07319900000000001
0.02056900000000006

我们可以看到,在处理1000×1000的数组时,使用eval函数就将性能提升了三倍多一点

所以在一般情况下,在处理大型数据时,我们最好使用eval或者query这样的高性能运算来节约运算时间

Pandas.eval()函数支持的运算

在最初的版本,Pandas.eval()函数支持的运算较少,但是从Pandas 0.16版本以后,Pandas.eval()函数就支持许多运算了,下面将一一介绍

算术运算符

Pandas.eval()支持所有的算数运算符,但是依旧需要以字符串的形式给出

DataFrame_1=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_2=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_3=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_4=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))


start_2=time.clock()
pd.eval('(DataFrame_1*DataFrame_2)/(DataFrame_3-DataFrame_4)')
end_2=time.clock()
print(end_2-start_2)
>>>
0.04514499999999999
比较运算符

Pandas.eval()函数支持所有的比较运算符,包括链式代数式

DataFrame_1=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_2=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_3=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_4=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))

start_2=time.clock()
pd.eval('(DataFrame_1<DataFrame_2)&(DataFrame_2<=DataFrame_3)&(DataFrame_3!=DataFrame_4)')
end_2=time.clock()
print(end_2-start_2)
>>>
0.023680999999999952
位运算符

Pandas.eval()函数支持&(按位与)和|(按位或)等位运算

DataFrame_1=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_2=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_3=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_4=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))

start_2=time.clock()
pd.eval('(DataFrame_1<DataFrame_2)|(DataFrame_2>DataFrame_3)&(DataFrame_3==DataFrame_4)')
end_2=time.clock()
print(end_2-start_2)
>>>
0.025263000000000035

此外,Pandas.eval()也支持and,or和not等封装器

对象属性与索引

Pandas.eval()函数也支持在字符串内对DataFrame对象进行索引获取列或者使用索引器来获取值

DataFrame_1=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_2=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_3=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))
DataFrame_4=pd.DataFrame(np.random.randint(0,10000,(1000,1000)))

start_2=time.clock()
ans=pd.eval('DataFrame_2.T[0]==DataFrame_2.iloc[0]')
end_2=time.clock()
print(end_2-start_2)
>>>
0.04004399999999997

但是Pandas.eval()并不支持在索引器中使用切片

其他运算

最后,再说下Pandas.eval()不支持的运算,包括函数调用,条件语句,循环,切片以及更复杂的运算

这些都可以借助Numexpr实现

DataFrame.eval()方法实现列间运算

Pandas.eval()是Pandas的顶层函数,但是DataFrame对象中其实还内置了eval()方法来实现Pandas.eval()支持的所有的运算

使用DataFrame对象的eval()方法的好处就是可以借助列名称直接进行运算

DataFrame_1=pd.DataFrame(np.random.randint(0,10,(3,4)),columns=list('ABCD'),index=list('abc'))
print(DataFrame_1)
start_2=time.clock()
print(DataFrame_1.eval('A+B-C/D'))
end_2=time.clock()
print(end_2-start_2)
>>>
   A  B  C  D
a  9  6  5  4
b  0  0  8  4
c  6  2  6  9
a    13.750000
b    -2.000000
c     7.333333
dtype: float64
0.007834000000000008

使用DataFrame.eval()方法新增列

实际上我们可以在计算的时候使用新的列名,然后指定eval()方法的inplace参数为True

DataFrame_1=pd.DataFrame(np.random.randint(0,10,(3,4)),columns=list('ABCD'),index=list('abc'))
print(DataFrame_1)
start_2=time.clock()
DataFrame_1.eval('E=A+B-C/D',inplace=True)
end_2=time.clock()
print(DataFrame_1)
print(end_2-start_2)
>>>
   A  B  C  D
a  8  0  7  5
b  2  8  9  1
c  5  7  2  0

   A  B  C  D    E
a  8  0  7  5  6.6
b  2  8  9  1  1.0
c  5  7  2  0 -inf
0.007609999999999895

DataFrame.query()方法

基于字符串代数式,DataFrame实现了query()方法,query()方法主要用于链式代数式来实现过滤,即会直接返回符合我们链式代数式的行

DataFrame_1=pd.DataFrame(np.random.randint(0,10,(3,4)),columns=list('ABCD'),index=list('abc'))
print(DataFrame_1)
start_2=time.clock()
print(DataFrame_1.query('A<B'))
end_2=time.clock()
print(end_2-start_2)
>>>
   A  B  C  D
a  6  7  4  9
b  6  6  1  4
c  8  9  3  2
   A  B  C  D
a  6  7  4  9
c  8  9  3  2
0.010602999999999918

性能决定时机

实际上我们到底用不用Pandas.eval(),DataFrame.eval()或者DataFrame.query()的关键在于:计算时间内存消耗

使用上面的三个函数 / 方法是比直接用Numpy的向量化运算要慢的

当处理小型数据集时,为了追求运算速度,我们可以直接使用Numpy的复合代数式,即便这样会开辟临时数组

当处理大型数据集时,直接使用Numpy的复合代数式开辟的临时数组可能会直接挤爆CPU的L1和L2缓存,因此我们最好还是使用三个函数 / 方法

Logo

汇聚原天河团队并行计算工程师、中科院计算所专家以及头部AI名企HPC专家,助力解决“卡脖子”问题

更多推荐