给定两个DataFrame,我想使用pd.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'))

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


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

我试图更好地理解engineparser参数,以确定如何最好地解决我的问题。我已经阅读了文档,但是区别并不清楚。


应使用哪些参数来确保我的代码以最高性能工作?
是否可以将表达式的结果分配回df2
此外,为了使事情更复杂,如何在字符串表达式中将x作为参数传递?


评论

我对问题和答案都表示赞同,因为我认为这是有用的原始材料。但是,作为可搜索的官方文档的一部分,它会更加有用(您可以通过请求请求进行更新)。

您能为pandas.MultiIndex编写其中之一吗?

@tel抱歉,由于几个问题,我不得不撤下并重新发布。 MultiIndex过滤规范的最终版本在这里。 :-)

#1 楼

此答案将深入研究pd.evaldf.querydf.eval提供的各种功能。

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

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-“缺少手册”


注意
在讨论的三个功能中,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")


在文档中可以找到详细列出所有受支持的功能和语法的列表。总之,



除左移运算符(<<)和右移运算符(>>)外的算术运算,例如df + 2 * pi / s ** 4 % 42-the_golden_ratio
比较运算,包括链式比较,例如2 < df < df2

布尔运算,例如df < df2 and df3 < 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,arcttanh,abs和
arctan2。




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

从列表中可以明显看出,您还可以传递expre涉及索引的部分,例如

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


解析器选择: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")


在这里,括号是必需的。按照常规方式执行此操作,将需要使用parens来覆盖按位运算符的更高优先级:

(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'。带列表和元组节点的==!=运算符的语义,使用in解析器时分别具有与not in'pandas'相似的语义。例如,

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


有效,并且将以与

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


OTOH相同的语义运行, pd.eval("df1 == [1, 2, 3]", parser='python')将引发NotImplementedError错误。

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

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

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

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


不幸的是,与numexpr引擎相比,此方法没有性能上的好处,并且很少有安全措施可确保不评估危险的表达,因此请自担风险使用!除非您知道自己在做什么,否则通常不建议将此选项更改为'python'


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

,例如,

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


此操作失败,因为未定义thresh。但是,这可以起作用:

pd.eval("df1 > thresh")

UndefinedVariableError: name 'thresh' is not defined


当您有要从字典提供的变量时,这很有用。另外,使用'python'引擎,您可以简单地执行以下操作:

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


但这可能比使用'numexpr'引擎并将字典传递给local_dictglobal_dict慢得多。希望这应该成为使用这些参数的令人信服的参数。

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

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

考虑问题中的示例


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



要为df2分配列“ D”,我们做

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


这不是对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


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

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

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


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

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


如果inplace设置为不带一个目标,会引发一个ValueError

尽管target参数很有趣,但是您几乎不需要使用它。

如果您想使用df.eval,您将使用涉及赋值的表达式:

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


注意
pd.eval的非预期用途之一是解析字面字符串,其方式与ast.literal_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


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

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


以及列表字符串:

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


但是,问题在于长度大于100的列表:
pd.eval(["[1, 2, 3]", "[4, 5]", "[10]"], engine='python')
[[1, 2, 3], [4, 5], [10]]


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




DataFrame.eval-与并置pandas.eval


如上所述,df.eval在后台调用pd.eval。 v0.23源代码显示以下内容:

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

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


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

更多内容,您可以继续阅读:何时使用DataFrame.eval()和pandas.eval()或python eval()

用法差异

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

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

指定列名

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

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)


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

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


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

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

df1.eval("A + B")


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

这些规则也适用于query

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

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

df1.eval("A + index")


query也是如此。

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

多行查询和赋值

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

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


...漂亮!但是,请注意,query不支持此功能。



eval v / s query-最终词

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

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


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

此方法使用顶级pandas.eval()函数评估
传递的查询。


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

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

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


要获取df1中“ A”> =“ B”的所有行,我们将使用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


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

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


但是,对于query来说,中间结果“ m”直接传递给loc,所以对于query来说,您只需需要执行

df1[m]
# df1.loc[m]

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


性能明智,它是完全相同的。

df1.query("A >= B")

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


但是后者更为简洁,可以一步完成相同的操作。

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

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

评论


您是否会进一步了解性能,因为这是OP中提出的问题之一?

–user3471881
18/12/18在21:42

@ user3471881我还没有非常深入地研究性能,因为它取决于每个人的用例,但是我对哪些后端和解析器选项通常最适合最大化性能进行了评论。

–cs95
18/12/19在3:41

我们已经从文档中知道numexpr通常可以提高速度,我只是希望对此进行更深入的分析,因为OP中的第一个问题专门提出了该问题,并表示无法在文档中找到答案。另一种选择是稍微修改OP问题以适合答案?

–user3471881
18/12/19在7:36

还是至少分析三种情况的性能:数据大小,查询复杂性和分配?顺便说一句-不用说,我喜欢这篇文章,非常感谢您在这里和整个论坛中的工作。否则我不会问这个:D

–user3471881
18/12/19在7:44

@ user3471881您的观点是正确的,我将考虑采取最佳措施。对我来说,最简单的方法是从OP中删除关于“绩效”的问题,但是为了对职位公正,请让我深入研究一下,看看是否可以更适当地解决这些问题。感谢您的反馈,非常感谢:-)

–cs95
18/12/19在7:46



#2 楼

不错的教程,但是请记住,如果您的数据集少于15,000行,那么在被其简单的语法吸引而广泛使用eval/query之前,它就存在严重的性能问题。

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

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