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

2024-03-11

我想使用一个或多个数据帧列执行算术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.eval's engine and parser确定如何最好地解决我的问题的论点。我已经经历过文档 https://pandas.pydata.org/pandas-docs/version/0.23.4/generated/pandas.eval.html,但我并不清楚其中的区别。

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

您可以使用 1)pd.eval() https://pandas.pydata.org/pandas-docs/version/0.23.4/generated/pandas.eval.html, 2) df.query() https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.query.html, or 3) df.eval() https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.eval.html。下面讨论它们的各种特性和功能。

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

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 https://pandas.pydata.org/pandas-docs/version/0.23.4/generated/pandas.eval.html

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

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

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

这里需要注意一些事项:

  1. 整个表达式是一个字符串
  2. df1, df2, and x引用全局命名空间中的变量,这些变量由eval解析表达式时
  3. 使用属性访问器索引来访问特定列。您还可以使用"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")

详细说明所有支持的功能和语法的列表可以在文档 https://pandas.pydata.org/pandas-docs/stable/enhancingperf.html#supported-syntax。总之,

  • 除左移之外的算术运算 (<<)和右移(>>) 运算符,例如df + 2 * pi / s ** 4 % 42- 黄金比例
  • 比较操作,包括链式比较,例如2 < df < df2
  • 布尔运算,例如df < df2 and df3 < df4 or not df_bool list and tuple文字,例如[1, 2] or (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 和 反正切2。

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

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

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

1a) 解析器选择:parser=...争论

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

使用默认解析器pandas,重载的按位运算符& and |使用 pandas 对象实现向量化 AND 和 OR 运算的运算符优先级与and and or. So,

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

Use parser='python'如果你想在评估字符串时保持与 python 的实际运算符优先级规则的一致性。

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

两种类型的解析器之间的另一个区别是语义== and !=具有列表和元组节点的运算符,其语义类似于in and not 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 error.

1b) 后端选择:engine=...争论

有两种选择 -numexpr(默认)和python. The numexpr选项使用numexpr https://github.com/pydata/numexpr针对性能进行了优化的后端。

使用 Python 后端,您的表达式的计算方式类似于将表达式传递给 Pythoneval功能。您可以灵活地在表达式内部执行更多操作,例如字符串操作。

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

不幸的是,这种方法提供了no性能优势超过numexpr引擎,并且很少有安全措施来确保危险的表达式不被计算,所以使用风险自负!一般不建议将此选项更改为'python'除非你知道自己在做什么。

1c) local_dict and global_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')

但这可能会是much比使用慢'numexpr'引擎并将字典传递给local_dict or global_dict。希望这能为这些参数的使用提供令人信服的论据。

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

这通常不是一个要求,因为通常有更简单的方法来执行此操作,但您可以分配结果pd.eval到一个实现的对象__getitem__例如dicts 和(你猜对了)DataFrames。

考虑问题中的例子

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

将“D”列分配给df2, we do

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, set 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

If 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

Note

One of pd.eval的意外用途是以非常类似于的方式解析文字字符串ast.literal_eval:

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

它还可以使用以下命令解析嵌套列表'python' engine:

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 https://stackoverflow.com/questions/48008191/attributeerror-pandasexprvisitor-object-has-no-attribute-visit-ellipsis-us.


2) DataFrame.eval https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.eval.htm:

正如刚才提到的,df.eval calls pd.eval在幕后,有一些并列的论点。这v0.23源代码 https://github.com/pandas-dev/pandas/blob/0.23.x/pandas/core/frame.py#L2861-L2962显示这个:

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() https://stackoverflow.com/questions/38725355/when-to-use-dataframe-eval-versus-pandas-eval-or-python-eval


2a) 使用差异

2a1) 数据帧表达式与系列表达式

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

2a2) 指定列名称

另一个主要区别是如何访问列。例如,要添加两列“A”和“B”df1,你会打电话给pd.eval具有以下表达式:

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

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

df1.eval("A + B")

因为,在这样的背景下df1,很明显“A”和“B”指的是列名称。

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

df1.eval("A + index")

Or, more generally, for any DataFrame with an index having 1 or more levels, you can refer to the kth level of the index in an expression using the variable "ilevel_k" which stands for "index at level k". IOW, the expression above can be written as df1.eval("A + ilevel_0").

这些规则也适用于df.query.

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

表达式内提供的变量前面必须带有“@”符号,以避免与列名混淆。

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

同样适用于query.

不用说,您的列名称必须遵循 Python 中有效标识符命名的规则才能在内部访问eval. See here https://docs.python.org/3/reference/lexical_analysis.html#identifiers有关命名标识符的规则列表。

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 vs query

这有助于思考df.query作为一个使用的函数pd.eval作为子程序。

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

该表达式的计算结果首先传递给DataFrame.loc如果由于多维键而失败 (例如,DataFrame),然后结果将传递给DataFrame.__getitem__().

该方法使用顶层pandas.eval()函数来评估 通过查询。

从相似度来看,query and df.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

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

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

m表示通过计算表达式“A >= B”生成的中间结果。然后我们用mask来过滤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

就性能而言,它是exactly相同。

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根据条件表达式查询或过滤行时。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

动态计算 Pandas 中公式的表达式 的相关文章

随机推荐