Python-使用pd.eval()在熊猫中进行动态表达评估

发布于 2021-02-02 23:17:35

给定两个数据框

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。具体来说,我想移植以下代码:

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

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

我试图更好地理解engineparser参数,以确定如何最好地解决我的问题。我已经浏览了文档,但是对我而言,区别并不明显。

  1. 应该使用什么参数来确保我的代码以最高性能工作?
  2. 有没有一种方法可以将表达式的结果赋值给df2?
  3. 另外,为了使事情更复杂,如何x在字符串表达式中作为参数传递?
关注者
0
被浏览
100
1 个回答
  • 面试哥
    面试哥 2021-02-02
    为面试而生,有面试问题,就找面试哥。

    这个答案潜入各种特性和功能的提供pd.eval,df.querydf.eval

    设置
    示例将涉及这些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.eval并在幕后df.query打电话 pd.eval。行为和用法在这三个功能上或多或少是一致的,有些语义上的细微变化将在后面强调。本节将介绍所有这三个功能共有的功能-包括(但不限于)允许的语法,优先级规则和关键字参数。

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

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

    这里要注意一些事情:

    1. 整个表达式是一个字符串
    2. df1,df2x引用全局命名空间中的变量,这些变量是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")
    

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

    • 除左移(<<)和右移(>>)运算符外的算术运算,例如df + 2 * pi / s ** 4 % 42-the_golden_ratio
    • 比较操作,包括链式比较,例如 2 < df < df2
    • 布尔运算(例如df < df2 and df3 < df4或)not 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,arcosh,arcsinh,arctanh,abs和arctan2。

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

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

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

    解析器选择:parser=…参数

    pd.eval解析表达式字符串以生成语法树时,支持两种不同的解析器选项:pandaspython。两者之间的主要区别通过稍有不同的优先级规则突出显示。

    使用默认解析器pandas,重载的逐位运算符&以及|与pandas对象实现向量化ANDOR的运算符的优先级与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().
    

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

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

    两种类型的解析器之间的另一个区别是带有listtuple节点的==and !=运算符的语义,在使用解析器时,它们的语义分别与in和相似。例如,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错误。

    后端选择:engine=…参数

    有两个选项- numexpr(默认)和python。该numexpr选项使用为性能优化的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’除非你知道自己在做什么,否则通常不建议将此选项更改为。

    local_dict和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')
    

    但是,这将可能是很多比使用较慢的'numexpr'发动机和传递一个字典local_dictglobal_dict。希望这应该为使用这些参数提供令人信服的论据。

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

    这通常不是必需的,因为通常有更简单的方法可以执行此操作,但是你可以将结果分配给pd.eval实现__getitem__诸如dicts和(猜对了)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则引发a。

    尽管该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'
    

    有关此错误,原因,修复和解决方法的更多信息,请参见此处。

    DataFrame.eval -与并置 pandas.eval
    如上所述,在后台df.eval调用pd.eval。的v0.23源代码示出了该:

    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。例如,没有简单的方法来指定pd.eval(“df1 + df2”)调用df1.eval或时的等效项df2.eval。

    指定列名

    另一个主要区别是如何访问列。例如,要在中添加两列“ 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")
    

    或者,更一般地,对于具有1或更多级的索引数据帧的任何,可以参考第k 个使用变量索引的水平在表达“ilevel_k”表示“ 我 ndex在等级k ”。IOW,上面的表达式可以写成df1.eval("A + ilevel_0")

    这些规则也适用于query

    在本地/全局命名空间中访问变量
    表达式内提供的变量必须以“ @”符号开头,以避免与列名混淆。

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

    同样的道理query。

    不用说,你的列名必须遵循python中有效标识符命名的规则,以便在内部访问eval。有关命名标识符的规则列表,请参见此处。

    多行查询和分配

    一个鲜为人知的事实是eval支持处理分配的多行表达式。例如,要基于某些列上的某些算术运算在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
    

    真漂亮 但是,请注意,此功能不受支持query

    evalv / s- query最终词

    它有助于将df.query其pd.eval视为用作子例程的函数。

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

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

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

    在相似的条件,query并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”生成的中间结果。然后,我们使用蒙版进行过滤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在基于条件表达式查询或过滤行时,请使用。



知识点
面圈网VIP题库

面圈网VIP题库全新上线,海量真题题库资源。 90大类考试,超10万份考试真题开放下载啦

去下载看看