从 Pandas 中的公式动态评估表达式

Posted

技术标签:

【中文标题】从 Pandas 中的公式动态评估表达式【英文标题】:Dynamically evaluate an expression from a formula in Pandas 【发布时间】:2019-05-15 17:45:36 【问题描述】:

我想使用pd.eval 对一个或多个数据帧列执行算术运算。具体来说,我想移植以下评估公式的代码:

x = 5
df2['D'] = df1['A'] + (df1['B'] * x)

...使用pd.eval 编码。使用pd.eval 的原因是我想自动化许多工作流,所以动态创建它们对我很有用。

我的两个输入数据帧是:

import pandas as pd
import numpy as np

np.random.seed(0)
df1 = pd.DataFrame(np.random.choice(10, (5, 4)), columns=list('ABCD'))
df2 = pd.DataFrame(np.random.choice(10, (5, 4)), columns=list('ABCD'))

df1
   A  B  C  D
0  5  0  3  3
1  7  9  3  5
2  2  4  7  6
3  8  8  1  6
4  7  7  8  1

df2
   A  B  C  D
0  5  9  8  9
1  4  3  0  3
2  5  0  2  3
3  8  1  3  3
4  3  7  0  1

我试图更好地理解pd.evalengineparser 参数以确定如何最好地解决我的问题。我已经通过the documentation,但我并不清楚区别。

    应使用哪些参数来确保我的代码以最高性能运行? 有没有办法将表达式的结果分配回df2? 另外,为了让事情变得更复杂,如何在字符串表达式中将x 作为参数传递?

【问题讨论】:

我对问题和答案都投了赞成票,因为我认为这是有用的原始材料。但是,作为可搜索的官方文档(您可以通过拉取请求更新)的一部分,它会更多有用。 你能给pandas.MultiIndex写一份吗? @tel 抱歉,由于一些问题,我不得不删除并重新发布。 MultiIndex 过滤规范的最终版本是here。 :-) 【参考方案1】:

您可以使用 1) pd.eval()、2) df.query() 或 3) df.eval()。下面讨论它们的各种特性和功能。

示例将涉及这些数据帧(除非另有说明)。

np.random.seed(0)
df1 = pd.DataFrame(np.random.choice(10, (5, 4)), columns=list('ABCD'))
df2 = pd.DataFrame(np.random.choice(10, (5, 4)), columns=list('ABCD'))
df3 = pd.DataFrame(np.random.choice(10, (5, 4)), columns=list('ABCD'))
df4 = pd.DataFrame(np.random.choice(10, (5, 4)), columns=list('ABCD'))

1) pandas.eval

这是 pandas doc 应包含的“缺失手册”。 注意: 在所讨论的三个函数中,pd.eval 是最重要的。 df.evaldf.query 致电 pd.eval 在引擎盖下。行为和使用或多或少 三个函数一致,有一些小的语义 稍后将突出显示的变化。本节将 引入所有三个函数共有的功能 - 这包括(但不限于)允许的语法、优先规则关键字参数。

pd.eval 可以计算可以由变量和/或文字组成的算术表达式。这些表达式必须作为字符串传递。因此,要回答问题,如上所述,您可以这样做

x = 5
pd.eval("df1.A + (df1.B * x)")

这里有几点需要注意:

    整个表达式是一个字符串 df1df2x 引用全局命名空间中的变量,这些变量在解析表达式时被 eval 拾取 使用属性访问器索引访问特定列。您也可以使用"df1['A'] + (df1['B'] * x)" 达到同样的效果。

我将在下面解释target=... 属性的部分中解决重新分配的具体问题。但是现在,这里有更简单的pd.eval 有效操作示例:

pd.eval("df1.A + df2.A")   # Valid, returns a pd.Series object
pd.eval("abs(df1) ** .5")  # Valid, returns a pd.DataFrame object

...等等。条件表达式也以相同的方式支持。以下语句都是有效的表达式,将由引擎进行评估。

pd.eval("df1 > df2")
pd.eval("df1 > 5")
pd.eval("df1 < df2 and df3 < df4")
pd.eval("df1 in [1, 2, 3]")
pd.eval("1 < 2 < 3")

可以在the documentation 中找到详细说明所有支持的功能和语法的列表。总之,

算术运算,除了左移 (&lt;&lt;) 和右移 (&gt;&gt;) 运算符,例如 df + 2 * pi / s ** 4 % 42 - the_golden_ratio 比较操作,包括链式比较,例如2 &lt; df &lt; df2 布尔运算,例如,df &lt; df2 and df3 &lt; df4not 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 和 arctan2.

文档的这一部分还指定了不受支持的语法规则,包括 set/dict 文字、if-else 语句、循环和推导以及生成器表达式。

从列表中,很明显你也可以传递涉及索引的表达式,比如

pd.eval('df1.A * (df1.index > 1)')

1a) 解析器选择:parser=... 参数

pd.eval 在解析表达式字符串以生成语法树时支持两种不同的解析器选项:pandaspython。两者之间的主要区别在于略有不同的优先规则。

使用默认解析器pandas,重载的按位运算符&amp;| 对pandas 对象实现矢量化AND 和OR 运算将具有与andor 相同的运算符优先级。所以,

pd.eval("(df1 > df2) & (df3 < df4)")

将与

相同
pd.eval("df1 > df2 & df3 < df4")
# pd.eval("df1 > df2 & df3 < df4", parser='pandas')

也一样

pd.eval("df1 > df2 and df3 < df4")

这里,括号是必需的。按照惯例,要做到这一点,需要括号来覆盖位运算符的更高优先级:

(df1 > df2) & (df3 < df4)

没有它,我们最终会得到

df1 > df2 & df3 < df4

ValueError: The truth value of a DataFrame is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().

如果您想在评估字符串时与 python 的实际运算符优先级规则保持一致,请使用 parser='python'

pd.eval("(df1 > df2) & (df3 < df4)", parser='python')

这两种解析器的另一个区别是==!= 运算符的语义具有列表和元组节点,当使用@ 时,它们的语义分别与innot in 相似。 987654388@解析器。例如,

pd.eval("df1 == [1, 2, 3]")

有效,并以与

相同的语义运行
pd.eval("df1 in [1, 2, 3]")

OTOH,pd.eval("df1 == [1, 2, 3]", parser='python') 将引发 NotImplementedError 错误。

1b) 后端选择:engine=... 参数

有两个选项 - numexpr(默认)和pythonnumexpr 选项使用针对性能进行了优化的 numexpr 后端。

使用 Python 后端,您的表达式的评估类似于将表达式传递给 Python 的 eval 函数。您可以灵活地执行更多内部表达式,例如字符串操作。

df = pd.DataFrame('A': ['abc', 'def', 'abacus'])
pd.eval('df.A.str.contains("ab")', engine='python')

0     True
1    False
2     True
Name: A, dtype: bool

不幸的是,与numexpr 引擎相比,此方法没有性能优势,并且很少有安全措施可以确保不评估危险的表达式,因此在风险自负!一般不建议将此选项更改为'python',除非您知道自己在做什么。

1c) local_dictglobal_dict 参数

有时,为表达式中使用但当前未在命名空间中定义的变量提供值很有用。您可以将字典传递给local_dict

例如:

pd.eval("df1 > thresh")

UndefinedVariableError: name 'thresh' is not defined

这失败了,因为thresh 没有定义。但是,这是可行的:

pd.eval("df1 > thresh", local_dict='thresh': 10)

当您要从字典中提供变量时,这很有用。或者,使用 Python 引擎,您可以简单地这样做:

mydict = 'thresh': 5
# Dictionary values with *string* keys cannot be accessed without
# using the 'python' engine.
pd.eval('df1 > mydict["thresh"]', engine='python')

但这可能会比使用'numexpr' 引擎并将字典传递给local_dictglobal_dict很多。希望这应该为使用这些参数提供一个令人信服的论据。

1d) target (+ inplace) 参数和赋值表达式

这通常不是必需的,因为通常有更简单的方法来执行此操作,但是您可以将pd.eval 的结果分配给实现__getitem__ 的对象,例如dicts,并且(您猜对了)数据帧。

考虑问题中的示例

x = 5
df2['D'] = df1['A'] + (df1['B'] * x)

要将列“D”分配给df2,我们这样做

pd.eval('D = df1.A + (df1.B * x)', target=df2)

   A  B  C   D
0  5  9  8   5
1  4  3  0  52
2  5  0  2  22
3  8  1  3  48
4  3  7  0  42

这不是对df2 的就地修改(但可以……继续阅读)。考虑另一个例子:

pd.eval('df1.A + df2.A')

0    10
1    11
2     7
3    16
4    10
dtype: int32

如果您想(例如)将其分配回 DataFrame,您可以使用 target 参数,如下所示:

df = pd.DataFrame(columns=list('FBGH'), index=df1.index)
df
     F    B    G    H
0  NaN  NaN  NaN  NaN
1  NaN  NaN  NaN  NaN
2  NaN  NaN  NaN  NaN
3  NaN  NaN  NaN  NaN
4  NaN  NaN  NaN  NaN

df = pd.eval('B = df1.A + df2.A', target=df)
# Similar to
# df = df.assign(B=pd.eval('df1.A + df2.A'))

df
     F   B    G    H
0  NaN  10  NaN  NaN
1  NaN  11  NaN  NaN
2  NaN   7  NaN  NaN
3  NaN  16  NaN  NaN
4  NaN  10  NaN  NaN

如果您想对df 执行就地突变,请设置inplace=True

pd.eval('B = df1.A + df2.A', target=df, inplace=True)
# Similar to
# df['B'] = pd.eval('df1.A + df2.A')

df
     F   B    G    H
0  NaN  10  NaN  NaN
1  NaN  11  NaN  NaN
2  NaN   7  NaN  NaN
3  NaN  16  NaN  NaN
4  NaN  10  NaN  NaN

如果 inplace 没有设置目标,则会引发 ValueError

虽然target 参数很有趣,但您很少需要使用它。

如果您想使用df.eval 执行此操作,您可以使用涉及赋值的表达式:

df = df.eval("B = @df1.A + @df2.A")
# df.eval("B = @df1.A + @df2.A", inplace=True)
df

     F   B    G    H
0  NaN  10  NaN  NaN
1  NaN  11  NaN  NaN
2  NaN   7  NaN  NaN
3  NaN  16  NaN  NaN
4  NaN  10  NaN  NaN

注意

pd.eval 的一个意外用途是以与ast.literal_eval 非常相似的方式解析文字字符串:

pd.eval("[1, 2, 3]")
array([1, 2, 3], dtype=object)

它还可以使用'python' 引擎解析嵌套列表:

pd.eval("[[1, 2, 3], [4, 5], [10]]", engine='python')
[[1, 2, 3], [4, 5], [10]]

还有字符串列表:

pd.eval(["[1, 2, 3]", "[4, 5]", "[10]"], engine='python')
[[1, 2, 3], [4, 5], [10]]

然而,问题在于长度大于 100 的列表:

pd.eval(["[1]"] * 100, engine='python') # Works
pd.eval(["[1]"] * 101, engine='python')

AttributeError: 'PandasExprVisitor' object has no attribute 'visit_Ellipsis'

更多信息可以在here找到这个错误、原因、修复和解决方法。


2)DataFrame.eval:

如上所述,df.eval 在后台调用pd.eval,并带有一些参数。 v0.23 source code 显示了这一点:

def eval(self, expr, inplace=False, **kwargs):

    from pandas.core.computation.eval import eval as _eval

    inplace = validate_bool_kwarg(inplace, 'inplace')
    resolvers = kwargs.pop('resolvers', None)
    kwargs['level'] = kwargs.pop('level', 0) + 1
    if resolvers is None:
        index_resolvers = self._get_index_resolvers()
        resolvers = dict(self.iteritems()), index_resolvers
    if 'target' not in kwargs:
        kwargs['target'] = self
    kwargs['resolvers'] = kwargs.get('resolvers', ()) + tuple(resolvers)
    return _eval(expr, inplace=inplace, **kwargs)

eval 创建参数,进行一些验证,然后将参数传递给 pd.eval

更多信息,您可以继续阅读:When to use DataFrame.eval() versus pandas.eval() or Python eval()


2a) 用法差异

2a1) DataFrame 表达式与系列表达式

对于与整个 DataFrames 关联的动态查询,您应该首选pd.eval。例如,当您调用df1.evaldf2.eval 时,没有简单的方法可以指定pd.eval("df1 + df2") 的等效项。

2a2) 指定列名

另一个主要区别是列的访问方式。例如,要在 df1 中添加两列“A”和“B”,您可以使用以下表达式调用 pd.eval

pd.eval("df1.A + df1.B")

使用 df.eval,您只需提供列名:

df1.eval("A + B")

因为在df1 的上下文中,很明显“A”和“B”指的是列名。

您还可以使用index 引用索引和列(除非索引已命名,在这种情况下您将使用该名称)。

df1.eval("A + index")

或者,更一般地说,对于任何具有 1 个或多个级别的索引的 DataFrame,您可以使用变量 "ilevel_k" 在表达式中引用索引的第 kth 级别 代表“index at level k”。 IOW,上面的表达式可以写成df1.eval("A + ilevel_0")

这些规则也适用于df.query

2a3) 访问本地/全局命名空间中的变量

在表达式中提供的变量必须以“@”符号开头,以避免与列名混淆。

A = 5
df1.eval("A > @A")

query 也是如此。

不用说,您的列名必须遵循 Python 中有效标识符命名的规则,才能在 eval 中访问。有关命名标识符的规则列表,请参阅 here。

2a4) 多行查询和赋值

一个鲜为人知的事实是eval 支持处理赋值的多行表达式(而query 不支持)。例如,要基于对某些列的一些算术运算在 df1 中创建两个新列“E”和“F”,并基于先前创建的“E”和“F”创建第三列“G”,我们可以这样做

df1.eval("""
E = A + B
F = @df2.A + @df2.B
G = E >= F
""")

   A  B  C  D   E   F      G
0  5  0  3  3   5  14  False
1  7  9  3  5  16   7   True
2  2  4  7  6   6   5   True
3  8  8  1  6  16   9   True
4  7  7  8  1  14  10   True

3) evalquery

df.query 视为使用pd.eval 作为子例程的函数会有所帮助。

通常,query(顾名思义)用于评估条件表达式(即产生真/假值的表达式)并返回与True 结果对应的行。然后将表达式的结果传递给loc(在大多数情况下)以返回满足表达式的行。根据文档,

这个表达式的求值结果首先传递给 DataFrame.loc 如果由于多维键而失败 (例如,一个 DataFrame)然后结果将被传递给 DataFrame.__getitem__().

此方法使用***pandas.eval() 函数来评估 通过查询。

在相似性方面,querydf.eval 在访问列名和变量的方式上都是相似的。

如上所述,两者之间的关键区别在于它们如何处理表达式结果。当您实际通过这两个函数运行表达式时,这一点变得很明显。例如,考虑

df1.A

0    5
1    7
2    2
3    8
4    7
Name: A, dtype: int32

df1.B

0    9
1    3
2    0
3    1
4    7
Name: B, dtype: int32

要获取df1 中“A”>=“B”的所有行,我们将像这样使用eval

m = df1.eval("A >= B")
m
0     True
1    False
2    False
3     True
4     True
dtype: bool

m 表示通过计算表达式“A >= B”生成的中间结果。然后我们使用掩码过滤df1

df1[m]
# df1.loc[m]

   A  B  C  D
0  5  0  3  3
3  8  8  1  6
4  7  7  8  1

但是,对于query,中间结果“m”直接传递给loc,所以对于query,您只需要这样做

df1.query("A >= B")

   A  B  C  D
0  5  0  3  3
3  8  8  1  6
4  7  7  8  1

性能方面,一模一样

df1_big = pd.concat([df1] * 100000, ignore_index=True)

%timeit df1_big[df1_big.eval("A >= B")]
%timeit df1_big.query("A >= B")

14.7 ms ± 33.9 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
14.7 ms ± 24.3 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

但后者更简洁,在一个步骤中表达相同的操作。

请注意,您也可以像这样使用 query 做一些奇怪的事情(例如,返回由 df1.index 索引的所有行)

df1.query("index")
# Same as df1.loc[df1.index] # Pointless,... I know

   A  B  C  D
0  5  0  3  3
1  7  9  3  5
2  2  4  7  6
3  8  8  1  6
4  7  7  8  1

但不要。

底线:根据条件表达式查询或过滤行时,请使用query

【讨论】:

您是否会更深入地研究性能,因为这是 OP 中提出的问题之一? @user3471881 我没有深入探讨性能,因为它取决于每个人的用例,但我已经评论了哪些后端和解析器选项最适合最大限度地提高性能。 我们已经从文档中知道numexpr 通常可以加快我们的速度,我只是期待对此进行更深入的分析,因为 OP 中的第一个问题专门提出了它并说它不能在文档中找不到答案。另一种方法是稍微改变 OP 问题以适应答案? 或者至少分析三个场景的性能:数据大小、查询复杂性和分配?顺便说一句 - 不用说我很喜欢这篇文章,并且非常感谢你在这里和整个论坛中的工作。否则我不会问这个:D @user3471881 你的观点是正确的,我会考虑最好的行动方案。对我来说,最简单的方法是从 OP 中删除关于“性能”的问题,但为了公正地对待这篇文章,让我更深入地挖掘一下,看看我是否能更恰当地解决这些问题。感谢您的反馈,非常感谢:-)【参考方案2】:

已经有很棒的教程,但请记住,在被其更简单的语法所吸引而疯狂使用 eval/query 之前,如果您的数据集少于 15,000 行,它会出现严重的性能问题。

在这种情况下,只需使用df.loc[mask1, mask2]

参考:Expression Evaluation via eval()

【讨论】:

以上是关于从 Pandas 中的公式动态评估表达式的主要内容,如果未能解决你的问题,请参考以下文章

Eclipse 可以动态评估表达式吗?

Scala中的“评估”

如何评估 MySQL 存储的_function_ 中的简单数学公式?

R从表中提取公式并在函数中使用[关闭]

为啥角度表达式评估为属性中的空字符串

数学表达式评估