提升性能#
在本教程的这一部分,我们将探讨如何使用 Cython、Numba 和 pandas.eval()
来加速对 pandas DataFrame
执行的某些函数。通常,使用 Cython 和 Numba 比使用 pandas.eval()
能够提供更大的加速,但需要更多的代码。
注意
除了遵循本教程中的步骤外,强烈建议对提升性能感兴趣的用户安装 pandas 的推荐依赖项。这些依赖项通常默认不安装,但如果安装了,将能带来速度提升。
Cython(为 pandas 编写 C 扩展)#
对于许多用例来说,用纯 Python 和 NumPy 编写 pandas 代码就足够了。然而,在某些计算密集型应用中,通过将工作分载到 cython,可以实现显著的加速。
本教程假设您已尽可能在 Python 中重构代码,例如尝试移除 for 循环并利用 NumPy 向量化。首先在 Python 中进行优化总是值得的。
本教程将介绍对慢速计算进行 cython 化处理的“典型”过程。我们使用了来自 Cython 文档的示例,但将其放在 pandas 的上下文中。我们最终的 cython 化解决方案比纯 Python 解决方案快大约 100 倍。
纯 Python#
我们有一个 DataFrame
,我们想对其按行应用一个函数。
In [1]: df = pd.DataFrame(
...: {
...: "a": np.random.randn(1000),
...: "b": np.random.randn(1000),
...: "N": np.random.randint(100, 1000, (1000)),
...: "x": "x",
...: }
...: )
...:
In [2]: df
Out[2]:
a b N x
0 0.469112 -0.218470 585 x
1 -0.282863 -0.061645 841 x
2 -1.509059 -0.723780 251 x
3 -1.135632 0.551225 972 x
4 1.212112 -0.497767 181 x
.. ... ... ... ..
995 -1.512743 0.874737 374 x
996 0.933753 1.120790 246 x
997 -0.308013 0.198768 157 x
998 -0.079915 1.757555 977 x
999 -1.010589 -1.115680 770 x
[1000 rows x 4 columns]
以下是纯 Python 中的函数
In [3]: def f(x):
...: return x * (x - 1)
...:
In [4]: def integrate_f(a, b, N):
...: s = 0
...: dx = (b - a) / N
...: for i in range(N):
...: s += f(a + i * dx)
...: return s * dx
...:
我们通过使用 DataFrame.apply()
(按行)来实现我们的结果
In [5]: %timeit df.apply(lambda x: integrate_f(x["a"], x["b"], x["N"]), axis=1)
84 ms +- 1.01 ms per loop (mean +- std. dev. of 7 runs, 10 loops each)
让我们使用 prun ipython magic 函数来看看在此操作期间时间花在了哪里
# most time consuming 4 calls
In [6]: %prun -l 4 df.apply(lambda x: integrate_f(x["a"], x["b"], x["N"]), axis=1) # noqa E999
605956 function calls (605938 primitive calls) in 0.171 seconds
Ordered by: internal time
List reduced from 163 to 4 due to restriction <4>
ncalls tottime percall cumtime percall filename:lineno(function)
1000 0.099 0.000 0.152 0.000 <ipython-input-4-c2a74e076cf0>:1(integrate_f)
552423 0.053 0.000 0.053 0.000 <ipython-input-3-c138bdd570e3>:1(f)
3000 0.003 0.000 0.013 0.000 series.py:1095(__getitem__)
3000 0.002 0.000 0.006 0.000 series.py:1220(_get_value)
绝大多数时间都花在 integrate_f
或 f
内部,因此我们将集中精力对这两个函数进行 cython 化。
普通 Cython#
首先我们需要将 Cython magic 函数导入 IPython
In [7]: %load_ext Cython
现在,我们只需将函数复制到 Cython 中
In [8]: %%cython
...: def f_plain(x):
...: return x * (x - 1)
...: def integrate_f_plain(a, b, N):
...: s = 0
...: dx = (b - a) / N
...: for i in range(N):
...: s += f_plain(a + i * dx)
...: return s * dx
...:
In [9]: %timeit df.apply(lambda x: integrate_f_plain(x["a"], x["b"], x["N"]), axis=1)
47.2 ms +- 366 us per loop (mean +- std. dev. of 7 runs, 10 loops each)
与纯 Python 方法相比,这已将性能提高了三分之一。
声明 C 类型#
我们可以为函数变量和返回类型添加注解,并使用 cdef
和 cpdef
来提升性能
In [10]: %%cython
....: cdef double f_typed(double x) except? -2:
....: return x * (x - 1)
....: cpdef double integrate_f_typed(double a, double b, int N):
....: cdef int i
....: cdef double s, dx
....: s = 0
....: dx = (b - a) / N
....: for i in range(N):
....: s += f_typed(a + i * dx)
....: return s * dx
....:
In [11]: %timeit df.apply(lambda x: integrate_f_typed(x["a"], x["b"], x["N"]), axis=1)
7.75 ms +- 23.9 us per loop (mean +- std. dev. of 7 runs, 100 loops each)
用 C 类型对函数添加注解后,性能比原始 Python 实现提高了十倍以上。
使用 ndarray#
重新分析性能时发现,时间花在为每一行创建一个 Series
,以及从索引和 Series 中调用 __getitem__
(每一行调用三次)。这些 Python 函数调用开销很大,可以通过传递一个 np.ndarray
来改进。
In [12]: %prun -l 4 df.apply(lambda x: integrate_f_typed(x["a"], x["b"], x["N"]), axis=1)
52533 function calls (52515 primitive calls) in 0.019 seconds
Ordered by: internal time
List reduced from 161 to 4 due to restriction <4>
ncalls tottime percall cumtime percall filename:lineno(function)
3000 0.003 0.000 0.012 0.000 series.py:1095(__getitem__)
3000 0.002 0.000 0.005 0.000 series.py:1220(_get_value)
3000 0.002 0.000 0.002 0.000 base.py:3777(get_loc)
3000 0.002 0.000 0.002 0.000 indexing.py:2765(check_dict_or_set_indexers)
In [13]: %%cython
....: cimport numpy as np
....: import numpy as np
....: cdef double f_typed(double x) except? -2:
....: return x * (x - 1)
....: cpdef double integrate_f_typed(double a, double b, int N):
....: cdef int i
....: cdef double s, dx
....: s = 0
....: dx = (b - a) / N
....: for i in range(N):
....: s += f_typed(a + i * dx)
....: return s * dx
....: cpdef np.ndarray[double] apply_integrate_f(np.ndarray col_a, np.ndarray col_b,
....: np.ndarray col_N):
....: assert (col_a.dtype == np.float64
....: and col_b.dtype == np.float64 and col_N.dtype == np.dtype(int))
....: cdef Py_ssize_t i, n = len(col_N)
....: assert (len(col_a) == len(col_b) == n)
....: cdef np.ndarray[double] res = np.empty(n)
....: for i in range(len(col_a)):
....: res[i] = integrate_f_typed(col_a[i], col_b[i], col_N[i])
....: return res
....:
Content of stderr:
In file included from /home/runner/micromamba/envs/test/lib/python3.10/site-packages/numpy/core/include/numpy/ndarraytypes.h:1929,
from /home/runner/micromamba/envs/test/lib/python3.10/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,
from /home/runner/micromamba/envs/test/lib/python3.10/site-packages/numpy/core/include/numpy/arrayobject.h:5,
from /home/runner/.cache/ipython/cython/_cython_magic_96d1519457caba8fa4f96b759be00659f51c6b18.c:1215:
/home/runner/micromamba/envs/test/lib/python3.10/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning "Using deprecated NumPy API, disable it with " "#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION" [-Wcpp]
17 | #warning "Using deprecated NumPy API, disable it with " \
| ^~~~~~~
此实现创建一个全零数组,并插入对每一行应用 integrate_f_typed
的结果。在 Cython 中循环遍历 ndarray
比循环遍历 Series
对象更快。
由于 apply_integrate_f
的类型被指定为接受 np.ndarray
,因此需要调用 Series.to_numpy()
来使用此函数。
In [14]: %timeit apply_integrate_f(df["a"].to_numpy(), df["b"].to_numpy(), df["N"].to_numpy())
834 us +- 2.87 us per loop (mean +- std. dev. of 7 runs, 1,000 loops each)
性能比之前的实现提高了近十倍。
禁用编译器指令#
现在大部分时间都花在 apply_integrate_f
中。禁用 Cython 的 boundscheck
和 wraparound
检查可以带来更多性能提升。
In [15]: %prun -l 4 apply_integrate_f(df["a"].to_numpy(), df["b"].to_numpy(), df["N"].to_numpy())
78 function calls in 0.001 seconds
Ordered by: internal time
List reduced from 21 to 4 due to restriction <4>
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.001 0.001 0.001 0.001 <string>:1(<module>)
1 0.000 0.000 0.001 0.001 {built-in method builtins.exec}
3 0.000 0.000 0.000 0.000 frame.py:4062(__getitem__)
3 0.000 0.000 0.000 0.000 base.py:541(to_numpy)
In [16]: %%cython
....: cimport cython
....: cimport numpy as np
....: import numpy as np
....: cdef np.float64_t f_typed(np.float64_t x) except? -2:
....: return x * (x - 1)
....: cpdef np.float64_t integrate_f_typed(np.float64_t a, np.float64_t b, np.int64_t N):
....: cdef np.int64_t i
....: cdef np.float64_t s = 0.0, dx
....: dx = (b - a) / N
....: for i in range(N):
....: s += f_typed(a + i * dx)
....: return s * dx
....: @cython.boundscheck(False)
....: @cython.wraparound(False)
....: cpdef np.ndarray[np.float64_t] apply_integrate_f_wrap(
....: np.ndarray[np.float64_t] col_a,
....: np.ndarray[np.float64_t] col_b,
....: np.ndarray[np.int64_t] col_N
....: ):
....: cdef np.int64_t i, n = len(col_N)
....: assert len(col_a) == len(col_b) == n
....: cdef np.ndarray[np.float64_t] res = np.empty(n, dtype=np.float64)
....: for i in range(n):
....: res[i] = integrate_f_typed(col_a[i], col_b[i], col_N[i])
....: return res
....:
Content of stderr:
In file included from /home/runner/micromamba/envs/test/lib/python3.10/site-packages/numpy/core/include/numpy/ndarraytypes.h:1929,
from /home/runner/micromamba/envs/test/lib/python3.10/site-packages/numpy/core/include/numpy/ndarrayobject.h:12,
from /home/runner/micromamba/envs/test/lib/python3.10/site-packages/numpy/core/include/numpy/arrayobject.h:5,
from /home/runner/.cache/ipython/cython/_cython_magic_3bb7bde31cdaf5ab952bfe5a612c6edef03550d0.c:1216:
/home/runner/micromamba/envs/test/lib/python3.10/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:17:2: warning: #warning "Using deprecated NumPy API, disable it with " "#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION" [-Wcpp]
17 | #warning "Using deprecated NumPy API, disable it with " \
| ^~~~~~~
In [17]: %timeit apply_integrate_f_wrap(df["a"].to_numpy(), df["b"].to_numpy(), df["N"].to_numpy())
622 us +- 672 ns per loop (mean +- std. dev. of 7 runs, 1,000 loops each)
但是,如果循环索引器 i
访问了数组中的无效位置,则会导致段错误 (segfault),因为内存访问没有被检查。有关 boundscheck
和 wraparound
的更多信息,请参阅 Cython 关于编译器指令的文档。
Numba(JIT 编译)#
静态编译 Cython 代码的另一种选择是使用 Numba 的动态即时 (JIT) 编译器。
Numba 允许您编写纯 Python 函数,通过使用 @jit
装饰器来将其 JIT 编译成本机机器指令,其性能可与 C、C++ 和 Fortran 相媲美。
Numba 的工作原理是在导入时、运行时或静态地(使用包含的 pycc 工具)使用 LLVM 编译器基础设施生成优化的机器码。Numba 支持将 Python 代码编译以在 CPU 或 GPU 硬件上运行,并且旨在与 Python 科学软件栈集成。
注意
@jit
编译会增加函数的运行时开销,因此性能优势可能不会显现,尤其是在使用小数据集时。考虑缓存您的函数,以避免每次运行函数时都产生编译开销。
Numba 可以通过两种方式与 pandas 一起使用
在选定的 pandas 方法中指定
engine="numba"
关键字定义您自己的用
@jit
装饰的 Python 函数,并将Series
或DataFrame
的底层 NumPy 数组(使用Series.to_numpy()
)传递给该函数
pandas Numba 引擎#
如果安装了 Numba,可以在选定的 pandas 方法中指定 engine="numba"
关键字,以使用 Numba 执行该方法。支持 engine="numba"
的方法还将有一个 engine_kwargs
关键字,它接受一个字典,允许指定布尔值的 "nogil"
、"nopython"
和 "parallel"
键,以传递给 @jit
装饰器。如果未指定 engine_kwargs
,则默认为 {"nogil": False, "nopython": True, "parallel": False}
,除非另有指定。
注意
在性能方面,首次使用 Numba 引擎运行函数会比较慢,因为 Numba 会有一些函数编译开销。但是,JIT 编译的函数会被缓存,后续调用会很快。一般来说,Numba 引擎在数据量较大(例如 100 万以上)时性能更佳。
In [1]: data = pd.Series(range(1_000_000)) # noqa: E225
In [2]: roll = data.rolling(10)
In [3]: def f(x):
...: return np.sum(x) + 5
# Run the first time, compilation time will affect performance
In [4]: %timeit -r 1 -n 1 roll.apply(f, engine='numba', raw=True)
1.23 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)
# Function is cached and performance will improve
In [5]: %timeit roll.apply(f, engine='numba', raw=True)
188 ms ± 1.93 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [6]: %timeit roll.apply(f, engine='cython', raw=True)
3.92 s ± 59 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
如果您的计算硬件包含多个 CPU,通过将 parallel
设置为 True
以利用多个 CPU,可以实现最大的性能提升。在内部,pandas 利用 numba 对 DataFrame
的列进行并行计算;因此,此性能优势仅对列数较多的 DataFrame
有益。
In [1]: import numba
In [2]: numba.set_num_threads(1)
In [3]: df = pd.DataFrame(np.random.randn(10_000, 100))
In [4]: roll = df.rolling(100)
In [5]: %timeit roll.mean(engine="numba", engine_kwargs={"parallel": True})
347 ms ± 26 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [6]: numba.set_num_threads(2)
In [7]: %timeit roll.mean(engine="numba", engine_kwargs={"parallel": True})
201 ms ± 2.97 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
自定义函数示例#
用 @jit
装饰的自定义 Python 函数可以通过使用 Series.to_numpy()
传递其 NumPy 数组表示形式来与 pandas 对象一起使用。
import numba
@numba.jit
def f_plain(x):
return x * (x - 1)
@numba.jit
def integrate_f_numba(a, b, N):
s = 0
dx = (b - a) / N
for i in range(N):
s += f_plain(a + i * dx)
return s * dx
@numba.jit
def apply_integrate_f_numba(col_a, col_b, col_N):
n = len(col_N)
result = np.empty(n, dtype="float64")
assert len(col_a) == len(col_b) == n
for i in range(n):
result[i] = integrate_f_numba(col_a[i], col_b[i], col_N[i])
return result
def compute_numba(df):
result = apply_integrate_f_numba(
df["a"].to_numpy(), df["b"].to_numpy(), df["N"].to_numpy()
)
return pd.Series(result, index=df.index, name="result")
In [4]: %timeit compute_numba(df)
1000 loops, best of 3: 798 us per loop
在此示例中,使用 Numba 比使用 Cython 更快。
Numba 也可以用来编写向量化函数,这类函数不需要用户显式地遍历向量中的每个观测值;向量化函数会自动应用于每一行。考虑以下将每个观测值加倍的示例
import numba
def double_every_value_nonumba(x):
return x * 2
@numba.vectorize
def double_every_value_withnumba(x): # noqa E501
return x * 2
# Custom function without numba
In [5]: %timeit df["col1_doubled"] = df["a"].apply(double_every_value_nonumba) # noqa E501
1000 loops, best of 3: 797 us per loop
# Standard implementation (faster than a custom function)
In [6]: %timeit df["col1_doubled"] = df["a"] * 2
1000 loops, best of 3: 233 us per loop
# Custom function with numba
In [7]: %timeit df["col1_doubled"] = double_every_value_withnumba(df["a"].to_numpy())
1000 loops, best of 3: 145 us per loop
注意事项#
Numba 最擅长加速将数值函数应用于 NumPy 数组的函数。如果您尝试用 @jit
装饰包含不受支持的 Python 或 NumPy 代码的函数,编译将回退到对象模式,这很可能不会加速您的函数。如果您希望 Numba 在无法以加速代码的方式编译函数时抛出错误,请向 Numba 传递参数 nopython=True
(例如 @jit(nopython=True)
)。有关 Numba 模式故障排除的更多信息,请参阅 Numba 故障排除页面。
如果线程层导致不安全行为,使用 parallel=True
(例如 @jit(parallel=True)
)可能会导致 SIGABRT
错误。在运行带 parallel=True
的 JIT 函数之前,您可以先指定一个安全的线程层。
通常,在使用 Numba 时如果遇到段错误 (SIGSEGV
),请将问题报告给 Numba 问题跟踪器。
通过 eval()
进行表达式求值#
顶层函数 pandas.eval()
实现了对 Series
和 DataFrame
的高性能表达式求值。表达式求值允许将操作表示为字符串,并且对于大型 DataFrame
,可以通过一次性求值算术和布尔表达式来潜在地提供性能提升。
注意
对于简单表达式或涉及小型 DataFrame 的表达式,不应使用 eval()
。实际上,对于较小的表达式或对象,eval()
比普通 Python 慢几个数量级。一个好的经验法则是仅当您的 DataFrame
包含超过 10,000 行时才使用 eval()
。
支持的语法#
pandas.eval()
支持以下操作
算术运算,左移 (
<<
) 和右移 (>>
) 运算符除外,例如df + 2 * pi / s ** 4 % 42 - the_golden_ratio
比较运算,包括链式比较,例如
2 < df < df2
布尔运算,例如
df < df2 and df3 < df4 or not df_bool
list
和tuple
字面量,例如[1, 2]
或(1, 2)
属性访问,例如
df.a
下标表达式,例如
df[0]
简单变量求值,例如
pd.eval("df")
(这不太有用)数学函数:
sin
,cos
,exp
,log
,expm1
,log1p
,sqrt
,sinh
,cosh
,tanh
,arcsin
,arccos
,arctan
,arccosh
,arcsinh
,arctanh
,abs
,arctan2
和log10
。
以下 Python 语法不允许使用
表达式
数学函数以外的函数调用。
is
/is not
操作if
表达式lambda
表达式list
/set
/dict
推导式字面量
dict
和set
表达式yield
表达式生成器表达式
仅由标量值组成的布尔表达式
语句
局部变量#
您必须通过在名称前面加上 @
字符来显式引用您想在表达式中使用的任何局部变量。这种机制对于 DataFrame.query()
和 DataFrame.eval()
都一样。例如,
In [18]: df = pd.DataFrame(np.random.randn(5, 2), columns=list("ab"))
In [19]: newcol = np.random.randn(len(df))
In [20]: df.eval("b + @newcol")
Out[20]:
0 -0.206122
1 -1.029587
2 0.519726
3 -2.052589
4 1.453210
dtype: float64
In [21]: df.query("b < @newcol")
Out[21]:
a b
1 0.160268 -0.848896
3 0.333758 -1.180355
4 0.572182 0.439895
如果您没有在局部变量前加上 @
,pandas 将抛出异常,告知您该变量未定义。
使用 DataFrame.eval()
和 DataFrame.query()
时,这允许您在表达式中拥有同名的局部变量和 DataFrame
列。
In [22]: a = np.random.randn()
In [23]: df.query("@a < a")
Out[23]:
a b
0 0.473349 0.891236
1 0.160268 -0.848896
2 0.803311 1.662031
3 0.333758 -1.180355
4 0.572182 0.439895
In [24]: df.loc[a < df["a"]] # same as the previous expression
Out[24]:
a b
0 0.473349 0.891236
1 0.160268 -0.848896
2 0.803311 1.662031
3 0.333758 -1.180355
4 0.572182 0.439895
警告
如果不能使用 @
前缀,因为在该上下文中未定义它,pandas.eval()
将抛出异常。
In [25]: a, b = 1, 2
In [26]: pd.eval("@a + b")
Traceback (most recent call last):
File ~/micromamba/envs/test/lib/python3.10/site-packages/IPython/core/interactiveshell.py:3577 in run_code
exec(code_obj, self.user_global_ns, self.user_ns)
Cell In[26], line 1
pd.eval("@a + b")
File ~/work/pandas/pandas/pandas/core/computation/eval.py:325 in eval
_check_for_locals(expr, level, parser)
File ~/work/pandas/pandas/pandas/core/computation/eval.py:167 in _check_for_locals
raise SyntaxError(msg)
File <string>
SyntaxError: The '@' prefix is not allowed in top-level eval calls.
please refer to your variables by name without the '@' prefix.
在这种情况下,您应该像在标准 Python 中一样直接引用变量。
In [27]: pd.eval("a + b")
Out[27]: 3
pandas.eval()
解析器#
有两种不同的表达式语法解析器。
默认的 'pandas'
解析器允许使用更直观的语法来表达类似查询的操作(比较、合取和析取)。特别地,&
和 |
运算符的优先级被设置为与其对应的布尔运算符 and
和 or
的优先级相同。
例如,上面的合取可以在没有括号的情况下编写。或者,您可以使用 'python'
解析器来强制执行严格的 Python 语义。
In [28]: nrows, ncols = 20000, 100
In [29]: df1, df2, df3, df4 = [pd.DataFrame(np.random.randn(nrows, ncols)) for _ in range(4)]
In [30]: expr = "(df1 > 0) & (df2 > 0) & (df3 > 0) & (df4 > 0)"
In [31]: x = pd.eval(expr, parser="python")
In [32]: expr_no_parens = "df1 > 0 & df2 > 0 & df3 > 0 & df4 > 0"
In [33]: y = pd.eval(expr_no_parens, parser="pandas")
In [34]: np.all(x == y)
Out[34]: True
同一个表达式也可以使用 and
关键字进行“与”连接
In [35]: expr = "(df1 > 0) & (df2 > 0) & (df3 > 0) & (df4 > 0)"
In [36]: x = pd.eval(expr, parser="python")
In [37]: expr_with_ands = "df1 > 0 and df2 > 0 and df3 > 0 and df4 > 0"
In [38]: y = pd.eval(expr_with_ands, parser="pandas")
In [39]: np.all(x == y)
Out[39]: True
pandas.eval()
引擎#
有两种不同的表达式引擎。
'numexpr'
引擎是性能更高的引擎,对于大型 DataFrame
,与标准 Python 语法相比可以带来性能提升。此引擎需要安装可选依赖项 numexpr
。
'python'
引擎通常没有用处,除了用于测试其他求值引擎之外。使用 eval()
并指定 engine='python'
不会获得任何性能优势,甚至可能会导致性能下降。
In [40]: %timeit df1 + df2 + df3 + df4
7.3 ms +- 24.9 us per loop (mean +- std. dev. of 7 runs, 100 loops each)
In [41]: %timeit pd.eval("df1 + df2 + df3 + df4", engine="python")
7.92 ms +- 70.6 us per loop (mean +- std. dev. of 7 runs, 100 loops each)
DataFrame.eval()
方法#
除了顶层函数 pandas.eval()
之外,您还可以在 DataFrame
的“上下文”中对表达式进行求值。
In [42]: df = pd.DataFrame(np.random.randn(5, 2), columns=["a", "b"])
In [43]: df.eval("a + b")
Out[43]:
0 -0.161099
1 0.805452
2 0.747447
3 1.189042
4 -2.057490
dtype: float64
任何有效的 pandas.eval()
表达式也都是有效的 DataFrame.eval()
表达式,额外的好处是您无需在您感兴趣的列名前加上 DataFrame
的名称。
此外,您可以在表达式中执行列的赋值。这使得公式化求值成为可能。赋值目标可以是新的列名或现有的列名,并且它必须是有效的 Python 标识符。
In [44]: df = pd.DataFrame(dict(a=range(5), b=range(5, 10)))
In [45]: df = df.eval("c = a + b")
In [46]: df = df.eval("d = a + b + c")
In [47]: df = df.eval("a = 1")
In [48]: df
Out[48]:
a b c d
0 1 5 5 10
1 1 6 7 14
2 1 7 9 18
3 1 8 11 22
4 1 9 13 26
返回一个带有新列或修改后的列的 DataFrame
副本,原始 DataFrame 不会改变。
In [49]: df
Out[49]:
a b c d
0 1 5 5 10
1 1 6 7 14
2 1 7 9 18
3 1 8 11 22
4 1 9 13 26
In [50]: df.eval("e = a - c")
Out[50]:
a b c d e
0 1 5 5 10 -4
1 1 6 7 14 -6
2 1 7 9 18 -8
3 1 8 11 22 -10
4 1 9 13 26 -12
In [51]: df
Out[51]:
a b c d
0 1 5 5 10
1 1 6 7 14
2 1 7 9 18
3 1 8 11 22
4 1 9 13 26
可以使用多行字符串执行多个列的赋值。
In [52]: df.eval(
....: """
....: c = a + b
....: d = a + b + c
....: a = 1""",
....: )
....:
Out[52]:
a b c d
0 1 5 6 12
1 1 6 7 14
2 1 7 8 16
3 1 8 9 18
4 1 9 10 20
在标准 Python 中的等效代码是
In [53]: df = pd.DataFrame(dict(a=range(5), b=range(5, 10)))
In [54]: df["c"] = df["a"] + df["b"]
In [55]: df["d"] = df["a"] + df["b"] + df["c"]
In [56]: df["a"] = 1
In [57]: df
Out[57]:
a b c d
0 1 5 5 10
1 1 6 7 14
2 1 7 9 18
3 1 8 11 22
4 1 9 13 26
eval()
性能比较#
pandas.eval()
在包含大型数组的表达式中表现良好。
In [58]: nrows, ncols = 20000, 100
In [59]: df1, df2, df3, df4 = [pd.DataFrame(np.random.randn(nrows, ncols)) for _ in range(4)]
DataFrame
算术运算
In [60]: %timeit df1 + df2 + df3 + df4
7.72 ms +- 56.9 us per loop (mean +- std. dev. of 7 runs, 100 loops each)
In [61]: %timeit pd.eval("df1 + df2 + df3 + df4")
2.89 ms +- 73.7 us per loop (mean +- std. dev. of 7 runs, 100 loops each)
DataFrame
比较运算
In [62]: %timeit (df1 > 0) & (df2 > 0) & (df3 > 0) & (df4 > 0)
6.08 ms +- 48.5 us per loop (mean +- std. dev. of 7 runs, 100 loops each)
In [63]: %timeit pd.eval("(df1 > 0) & (df2 > 0) & (df3 > 0) & (df4 > 0)")
9.32 ms +- 24.1 us per loop (mean +- std. dev. of 7 runs, 100 loops each)
轴不对齐的 DataFrame
算术运算
In [64]: s = pd.Series(np.random.randn(50))
In [65]: %timeit df1 + df2 + df3 + df4 + s
12.7 ms +- 69.2 us per loop (mean +- std. dev. of 7 runs, 100 loops each)
In [66]: %timeit pd.eval("df1 + df2 + df3 + df4 + s")
3.61 ms +- 41.1 us per loop (mean +- std. dev. of 7 runs, 100 loops each)
注意
诸如以下的操作
1 and 2 # would parse to 1 & 2, but should evaluate to 2
3 or 4 # would parse to 3 | 4, but should evaluate to 3
~1 # this is okay, but slower when using eval
应该在 Python 中执行。如果您尝试对非 bool
或 np.bool_
类型的标量操作数执行任何布尔/位运算,将抛出异常。
这是一张图,显示了 pandas.eval()
的运行时间与计算中涉及的 DataFrame 大小的函数关系。两条线代表两个不同的引擎。

只有当您的 DataFrame
包含大约 100,000 行以上时,您才能看到使用 pandas.eval()
配合 numexpr
引擎带来的性能优势。
此图是使用包含 3 列的 DataFrame
创建的,每列包含使用 numpy.random.randn()
生成的浮点数值。
使用 numexpr
进行表达式求值的限制#
会产生对象 dtype 或由于 NaT
而涉及日期时间操作的表达式必须在 Python 空间中求值,但表达式的一部分仍然可以用 numexpr
求值。例如
In [67]: df = pd.DataFrame(
....: {"strings": np.repeat(list("cba"), 3), "nums": np.repeat(range(3), 3)}
....: )
....:
In [68]: df
Out[68]:
strings nums
0 c 0
1 c 0
2 c 0
3 b 1
4 b 1
5 b 1
6 a 2
7 a 2
8 a 2
In [69]: df.query("strings == 'a' and nums == 1")
Out[69]:
Empty DataFrame
Columns: [strings, nums]
Index: []
比较的数值部分 (nums == 1
) 将由 numexpr
求值,而比较的对象部分 ("strings == 'a'
) 将由 Python 求值。