[ Pandas version: 1.0.1 ]
Pandas数据科学生态环境的强大力量建立在NumPy与Pandas的基础上,并通过直观的语法将基本操作转换成C语言:在NumPy里是向量化/广播运算,在Pandas里是分组型的运算。
虽然这些抽象功能可以简洁高效地解决许多问题,但是它们经常需要创建临时中间对象,这样就会占用大量的计算时间与内存。
Pandas从0.13版本开始就引入了实验性工具,让用户可以直接运行C语言速度的操作,不需要十分费力地配置中间数组。它们就是eval()
和query()
函数,都依赖于Numexpr程序包。
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风格的字符串代数式进行运算)
eval()
和query()
工具也是基于Numexpr实现的import numexpr
mask_numexpr = numexpr.evaluate('(x > 0.5) & (y < 0.5)')
np.allclose(mask, mask_numexpr)
# True
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
# 创建一个整数类型的DataFrame
df1, df2, df3, df4, df5 = (pd.DataFrame(rng.randint(0, 100, (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()
支持| (or), & (and), ~ (not)
等位运算符,另外还可以在布尔类型的代数式中使用and
和or
等字面值
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
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实现)。
由于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
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
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()
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
在考虑要不要用这两个函数时,需要思考两个方面:计算时间和内存消耗,而内存消耗是更重要的影响因素。
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]
eval()
和query()
代数式# 可以通过这个方法大概估算变量的内存消耗:
df.values.nbytes
# 32000
在性能方面,即使没有使用最大的系统内存,eval()
的计算速度也比普通方法快。
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数据科学手册》