关于python:使用pd.eval()在pandas中进行动态表达式评估

Dynamic Expression Evaluation in pandas using pd.eval()

给出两个DataFrame

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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.eval对一个或多个列执行算术运算。 具体来说,我想移植以下代码:

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

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

我想更好地理解engineparser参数,以确定如何最好地解决我的问题。 我已经阅读了文档,但差异并不明确。

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

  • 这个答案深入研究了pd.evaldf.querydf.eval提供的各种特性和功能。

    建立
    示例将涉及这些DataFrame(除非另有说明)。

    1
    2
    3
    4
    5
    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'))

    pandas.eval -"失踪手册"

    Note
    Of the three functions being discussed, pd.eval is the most important. df.eval and df.query call
    pd.eval under the hood. Behaviour and usage is more or less
    consistent across the three functions, with some minor semantic
    variations which will be highlighted later. This section will
    introduce functionality that is common across all the three functions - this includes, (but not limited to) allowed syntax, precedence rules, and keyword arguments.

    Ok.

    pd.eval可以评估可以包含变量和/或文字的算术表达式。这些表达式必须作为字符串传递。所以,要回答上述问题,你可以做到

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

    有些事情需要注意:

  • 整个表达式是一个字符串
  • df1df2x指的是全局命名空间中的变量,这些变量在解析表达式时由eval选取
  • 使用属性访问器索引访问特定列。您也可以使用"df1['A'] + (df1['B'] * x)"来达到相同的效果。
  • 我将在解释下面target=...属性的部分中解决重新分配的具体问题。但是现在,以下是使用pd.eval进行有效操作的更简单示例:

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

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

    1
    2
    3
    4
    5
    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")

    可以在文档中找到详细说明所有支持的功能和语法的列表。综上所述,

  • Arithmetic operations except for the left shift (<<) and right shift (>>) operators, e.g., df + 2 * pi / s ** 4 % 42 - the_golden_ratio
  • Comparison operations, including chained comparisons, e.g., 2 < df < df2
  • Boolean operations, e.g., df < df2 and df3 < df4 or not df_bool
    list and tuple literals, e.g., [1, 2] or (1, 2)
  • Attribute access, e.g., df.a
  • Subscript expressions, e.g., df[0]
  • Simple variable evaluation, e.g., pd.eval('df') (this is not very useful)
  • Math functions: sin, cos, exp, log, expm1, log1p, sqrt, sinh, cosh, tanh, arcsin, arccos, arctan, arccosh, arcsinh, arctanh, abs and
    arctan2.
  • Ok.

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

    从列表中可以看出,您还可以传递涉及索引的表达式,例如

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

    解析器选择:parser=...参数

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

    使用默认解析器pandas,使用pandas对象实现向量化AND和OR运算的重载位运算符&|将具有与and和`或者相同的运算符优先级。所以,

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

    将是一样的

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

    而且也一样

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

    在这里,括号是必要的。为了做到这一点,传统上,parens将被要求覆盖按位运算符的更高优先级:

    1
    (df1 > df2) & (df3 < df4)

    没有它,我们最终会

    1
    2
    3
    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'

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

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

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

    是有效的,并将以与之相同的语义运行

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

    OTOH,pd.eval("df1 == [1, 2, 3]", parser='python')会抛出NotImplementedError错误。

    后端选择:engine=...参数

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

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

    1
    2
    3
    4
    5
    6
    7
    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'

    local_dictglobal_dict参数

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

    例如,

    1
    2
    3
    pd.eval("df1 > thresh")

    UndefinedVariableError: name 'thresh' is not defined

    这会失败,因为未定义thresh。但是,这有效:

    1
    pd.eval("df1 > x", local_dict={'thresh': 10})

    当您从字典中提供变量时,这非常有用。或者,使用'python'引擎,您可以简单地执行此操作:

    1
    2
    3
    4
    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要慢得多。希望这应该为使用这些参数提供令人信服的论据。

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

    这通常不是必需的,因为通常有更简单的方法,但您可以将pd.eval的结果分配给实现__getitem__的对象,例如dict s和(您猜对了)DataFrames。

    考虑问题中的示例

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

    要将列"D"分配给df2,我们可以

    1
    2
    3
    4
    5
    6
    7
    8
    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的就地修改(但它可以......继续阅读)。考虑另一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    pd.eval('df1.A + df2.A')

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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执行此操作,则可以使用涉及赋值的表达式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    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非常相似的方式解析文字字符串:

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

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

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

    和字符串列表:

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

    但是,问题是长度大于10的列表:

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

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

    可以在此处找到更多信息,此错误,原因,修复和解决方法。

    DataFrame.eval - 与pandas.eval的并置

    如上所述,df.eval在引擎盖下调用pd.eval。 v0.23源代码显示了这一点:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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

    有关更多信息,您可以阅读:何时使用DataFrame.eval()与pandas.eval()或python eval()

    用法差异

    使用DataFrames v / s系列表达式的表达式

    对于与整个DataFrame相关联的动态查询,您应该更喜欢pd.eval。例如,当您调用df1.evaldf2.eval时,没有简单的方法来指定等效的pd.eval("df1 + df2")

    指定列名称

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

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

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

    1
    df1.eval("A + B")

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

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

    1
    df1.eval("A + index")

    或者,更一般地,对于索引具有1个或更多级别的任何DataFrame,可以使用变量"ilevel_k"(表示"级别为k的索引")来引用表达式中索引的第k级。 IOW,上面的表达式可以写成df1.eval("A + ilevel_0")

    这些规则也适用于query

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

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

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

    query /同样如此

    毫无疑问,您的列名必须遵循python中有效标识符命名的规则才能在eval中访问。有关命名标识符的规则列表,请参见此处。

    多行查询和分配

    一个鲜为人知的事实是eval支持处理赋值的多行表达式。例如,要根据某些列上的某些算术运算在df1中创建两个新列"E"和"F",并根据先前创建的"E"和"F"创建第三列"G",我们可以做

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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

    ...俏皮!但请注意,query不支持此功能。

    eval v / s query - 最后一句话

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

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

    The result of the evaluation of this expression is first passed to
    DataFrame.loc and if that fails because of a multidimensional key
    (e.g., a DataFrame) then the result will be passed to
    DataFrame.__getitem__().

    Ok.

    This method uses the top-level pandas.eval() function to evaluate the
    passed query.

    Ok.

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    df1.A

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

    df2.B

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

    要获取df1中"A"> ="B"的所有行,我们将使用eval,如下所示:

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

    m表示通过评估表达式"A> = B"生成的中间结果。然后我们使用掩码来过滤df1

    1
    2
    3
    4
    5
    6
    7
    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,您只需要执行

    1
    2
    3
    4
    5
    6
    df1.query("A >= B")

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

    性能方面,它完全相同。

    1
    2
    3
    4
    5
    6
    7
    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索引的所有行)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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

    好。


    已经很好的教程了,但请记住,在通过其更简单的语法吸引使用eval/query之前,如果您的数据集少于15,000行,则会出现严重的性能问题。

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

    参考:https://pandas.pydata.org/pandas-docs/version/0.22/enhancingperf.html#enhancingperf-eval

    enter image description here