提高性能#

在本教程的这一部分中,我们将研究如何DataFrame使用 Cython、Numba 和pandas.eval().一般来说,使用 Cython 和 Numba 可以比使用 Cython 和 Numba 提供更大的加速,pandas.eval() 但需要更多代码。

笔记

除了遵循本教程中的步骤之外,强烈建议对增强性能感兴趣的用户安装 推荐的pandas 依赖项。默认情况下通常不会安装这些依赖项,但如果存在,将会提高速度。

Cython(为 pandas 编写 C 扩展)#

对于许多用例来说,用纯 Python 和 NumPy 编写 pandas 就足够了。然而,在一些计算量大的应用程序中,可以通过将工作卸载到cython来实现相当大的加速。

本教程假设您已在 Python 中进行了尽可能多的重构,例如尝试删除 for 循环并使用 NumPy 矢量化。首先在 Python 中进行优化总是值得的。

本教程将介绍对慢速计算进行 cython 化的“典型”过程。我们使用Cython 文档中的示例 ,但在 pandas 的上下文中。我们最终的 cythonized 解决方案比纯 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)
74.9 ms +- 728 us per loop (mean +- std. dev. of 7 runs, 10 loops each)

让我们使用prun ipython 魔术函数来看看这个操作期间花费的时间在哪里:

# 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.167 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.097    0.000    0.148    0.000 <ipython-input-4-c2a74e076cf0>:1(integrate_f)
   552423    0.051    0.000    0.051    0.000 <ipython-input-3-c138bdd570e3>:1(f)
     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)

到目前为止,大部分时间都花在 或 中integrate_ff因此我们将集中精力对这两个函数进行 cython化。

普通 Cython #

首先,我们需要将 Cython 魔术函数导入 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)
46.6 ms +- 466 us per loop (mean +- std. dev. of 7 runs, 10 loops each)

与纯 Python 方法相比,这将性能提高了三分之一。

声明 C 类型#

我们可以对函数变量和返回类型进行注释以及使用cdefcpdef来提高性能:

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.76 ms +- 83.8 us per loop (mean +- std. dev. of 7 runs, 100 loops each)

与原始 Python 实现相比,使用 C 类型注释函数的性能提高了十倍以上。

使用 ndarray #

重新分析时,需要花费时间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 中循环 anndarray比循环Seriesobject 更快。

由于apply_integrate_f输入 是为了接受np.ndarraySeries.to_numpy() 因此需要调用才能使用此函数。

In [14]: %timeit apply_integrate_f(df["a"].to_numpy(), df["b"].to_numpy(), df["N"].to_numpy())
834 us +- 4.04 us per loop (mean +- std. dev. of 7 runs, 1,000 loops each)

性能比之前的实施提高了近十倍。

禁用编译器指令#

现在大部分时间都花在了apply_integrate_f。禁用 Cythonboundscheckwraparound检查可以提高性能。

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())
620 us +- 2.65 us per loop (mean +- std. dev. of 7 runs, 1,000 loops each)

但是,访问数组中无效位置的循环索引器i会导致段错误,因为不检查内存访问。有关boundscheck和 的更多信息,请参阅有关编译器指令wraparound的 Cython 文档 。

Numba(JIT 编译)#

静态编译 Cython 代码的替代方法是使用带有Numba 的动态即时 (JIT) 编译器。

Numba 允许您编写纯 Python 函数,通过用@jit.

Numba 的工作原理是在导入时、运行时或静态(使用附带的 pycc 工具)使用 LLVM 编译器基础设施生成优化的机器代码。 Numba 支持 Python 编译以在 CPU 或 GPU 硬件上运行,并且旨在与 Python 科学软件堆栈集成。

笔记

编译@jit会增加函数运行时的开销,因此可能无法实现性能优势,尤其是在使用小数据集时。考虑缓存您的函数以避免每次运行函数时的编译开销。

Numba 可以通过两种方式与 pandas 一起使用:

  1. engine="numba"在 select pandas 方法中指定关键字

  2. 定义您自己的 Python 函数,用或@jit修饰的底层 NumPy 数组(使用)传递到函数中SeriesDataFrameSeries.to_numpy()

熊猫 Numba 引擎#

如果安装了 Numba,则可以engine="numba"在选择 pandas 方法中指定使用 Numba 执行该方法。支持的方法engine="numba"还将有一个engine_kwargs关键字,该关键字接受允许指定的字典 "nogil""nopython"以及"parallel"带有布尔值的键以传递到@jit装饰器中。如果engine_kwargs没有指定,除非另有说明,否则默认为。{"nogil": False, "nopython": True, "parallel": False}

笔记

在性能方面,第一次使用 Numba 引擎运行函数会很慢, 因为 Numba 会有一些函数编译开销。不过,JIT编译的函数会被缓存,后续调用会很快。一般来说,Numba 引擎在处理大量数据点(例如 1+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)

parallel如果您的计算硬件包含多个 CPU,则可以通过设置为True 利用 1 个以上的 CPU来实现最大的性能增益。在内部,pandas 利用 numba 对 a 的列进行并行计算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)

自定义函数示例#

用 修饰的自定义 Python 函数@jit可以通过使用Series.to_numpy().

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 数组的函数。如果您尝试使用包含不受支持的PythonNumPy@jit代码的函数 ,编译将恢复对象模式,这很可能不会加速您的函数。如果您希望 Numba 在无法以加速代码的方式编译函数时抛出错误,请向 Numba 传递参数 (例如)。有关 Numba 模式故障排除的更多信息,请参阅Numba 故障排除页面nopython=True@jit(nopython=True)

如果线程层导致不安全行为,则使用parallel=True(例如@jit(parallel=True))可能会导致。SIGABRT您可以 在使用 运行 JIT 函数之前首先指定一个安全线程层parallel=True

通常,如果您SIGSEGV在使用 Numba 时遇到段错误 ( ),请将该问题报告给Numba 问题跟踪器。

通过#进行表达式评估eval()

顶级函数实现和 的pandas.eval()高性能表达式求值 。表达式求值允许将操作表示为字符串,并且可以通过同时求值算术和布尔表达式来提高性能。SeriesDataFrameDataFrame

笔记

您不应该用于eval()简单表达式或涉及小型 DataFrame 的表达式。事实上, eval()对于较小的表达式或对象,它比普通 Python 慢许多数量级。一个好的经验法则是仅eval()当您的 DataFrame行数超过 10,000 时才使用。

支持的语法#

这些操作由以下支持pandas.eval()

  • 除左移 ( <<) 和右移 ( >>) 运算符之外的算术运算,例如,df + 2 * pi / s ** 4 % 42 - the_golden_ratio

  • 比较操作,包括链式比较,例如,2 < df < df2

  • 布尔运算,例如df < df2 and df3 < df4 or not df_bool

  • listtuple文字,例如,或[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,arctan2log10

不允许使用以下 Python 语法:

  • 表达式

    • 数学函数以外的函数调用。

    • is/运营is not

    • if表达式

    • lambda表达式

    • list//理解setdict

    • 文字dictset表达式

    • yield表达式

    • 生成器表达式

    • 仅包含标量值的布尔表达式

  • 声明

    • 不允许使用简单复合 语句。这包括forwhile、 和 if

局部变量#

您必须通过将字符放在名称前面来显式引用要在表达式中使用的任何局部变量。该机制对于和@都是相同的。例如,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()and时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相同的表达式也可以与单词“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

这里的and运算andor与 Python 中的优先级相同。

pandas.eval()引擎

有两种不同的表达引擎。

'numexpr'引擎是性能更高的引擎,与大型DataFrame.该引擎需要numexpr安装可选的依赖项。

除了测试其他评估引擎之外,该'python'引擎通常没有用处。使用with不会带来任何性能优势,并且可能会导致性能下降。eval()engine='python'

In [40]: %timeit df1 + df2 + df3 + df4
7.42 ms +- 81.8 us per loop (mean +- std. dev. of 7 runs, 100 loops each)
In [41]: %timeit pd.eval("df1 + df2 + df3 + df4", engine="python")
8.11 ms +- 161 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返回具有新列或修改列的副本,并且原始框架保持不变。

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.34 ms +- 117 us per loop (mean +- std. dev. of 7 runs, 100 loops each)
In [61]: %timeit pd.eval("df1 + df2 + df3 + df4")
2.85 ms +- 58.8 us per loop (mean +- std. dev. of 7 runs, 100 loops each)

DataFrame比较:

In [62]: %timeit (df1 > 0) & (df2 > 0) & (df3 > 0) & (df4 > 0)
5.98 ms +- 37 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.38 ms +- 36.7 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.6 ms +- 105 us per loop (mean +- std. dev. of 7 runs, 100 loops each)
In [66]: %timeit pd.eval("df1 + df2 + df3 + df4 + s")
3.69 ms +- 62 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()计算中涉及的帧大小的函数的运行时间。这两条线是两个不同的发动机。

../_images/eval-perf.png

仅当您的 行数超过大约 100,000 时,您才会看到使用该numexpr引擎的性能优势。pandas.eval()DataFrame

该图是使用DataFrame3 列创建的,每列包含使用 生成的浮点值numpy.random.randn()

带有# 的表达式求值限制numexpr

由于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: []

比较的数字部分 ( ) 将由 Python 计算 ,比较的对象部分 ( ) 将由 Python 计算。nums == 1numexpr"strings == 'a'