从 Pandas 中的公式动态计算表达式

Dynamically evaluate an expression from a formula in Pandas

我想使用 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,但我并不清楚其中的区别。

  1. 应该使用哪些参数来确保我的代码以最佳性能运行?
  2. 有没有办法将表达式的结果赋值回 df2
  3. 此外,为了让事情变得更复杂,我如何将 x 作为参数传递到字符串表达式中?

您可以使用 1) pd.eval(), 2) df.query(), or 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

This is the "Missing Manual" that pandas doc should contain. 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.

pd.eval 可以计算由变量 and/or 文字组成的算术表达式。这些表达式必须作为字符串传递。所以,要回答问题,你可以

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

这里有几点需要注意:

  1. 整个表达式是一个字符串
  2. df1df2x引用全局命名空间中的变量,这些在解析表达式
  3. 时由eval选取
  4. 使用属性访问器索引访问特定列。您也可以使用 "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 中找到详细说明所有支持的功能和语法的列表。综上所述,

  • 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.

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

从表中可以看出,也可以传递涉及索引的表达式,如

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

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

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

使用默认解析器 pandas,重载的按位运算符 &| 实现了与 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 分别在使用 'pandas' 解析器时。例如,

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__ 的对象,例如 dict s,和(你猜对了)DataFrames。

考虑问题中的例子

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'

可以找到此错误、原因、修复和解决方法的更多信息


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 <b>_eval(expr, inplace=inplace, **kwargs)</b>

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

更多内容,您可以继续阅读:


2a) 用法差异

2a1) 数据框表达式与序列表达式

对于与整个 DataFrame 关联的动态查询,您应该更喜欢 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" 代表“iindex at level k”。 IOW,上面的表达式可以写成 df1.eval("A + ilevel_0").

这些规则也适用于 df.query

2a3) 访问 Local/Global 命名空间中的变量

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

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) eval 对比 query

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

通常,query(顾名思义)用于计算条件表达式(即产生 True/False 值的表达式)和 return 对应于 True 结果。然后将表达式的结果传递给 loc(在大多数情况下)到 return 满足该表达式的行。根据文档,

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__().

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

就相似性而言,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 做一些奇怪的事情(比如,return 所有由 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

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

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

参考:Expression Evaluation via eval()