【问题标题】:Inconsistent behavior of jitted functionjited 函数的不一致行为
【发布时间】:2018-12-26 23:59:25
【问题描述】:

我有这样一个非常简单的函数:

import numpy as np
from numba import jit
import pandas as pd

@jit
def f_(n, x, y, z):
    for i in range(n):
        z[i] = x[i] * y[i] 

f_(df.shape[0], df["x"].values, df["y"].values, df["z"].values)

我经过的地方

df = pd.DataFrame({"x": [1, 2, 3], "y": [3, 4, 5], "z": np.NaN})

我希望该函数会像这样修改数据z 列:

>>> f_(df.shape[0], df["x"].values, df["y"].values, df["z"].values)
>>> df

   x  y     z
0  1  3   3.0
1  2  4   8.0
2  3  5  15.0

这在大多数情况下都可以正常工作,但不知何故无法修改其他数据。

我仔细检查了一些东西,然后:

  • 我尚未确定任何可能导致此问题的数据点问题。
  • 我看到打印结果时数据已按预期修改。
  • 如果我从函数返回 z 数组,它会按预期进行修改。

不幸的是,我无法将问题减少到最小的可重现情况。例如,删除不相关的列似乎“修复”了无法减少的问题。

我是否以不打算使用的方式使用jit?我应该注意哪些边境案件?或者它可能是一个错误?

编辑

我找到了问题的根源。当数据包含重复的列名时会发生这种情况:

>>> df_ = pd.read_json('{"schema": {"fields":[{"name":"index","type":"integer"},{"name":"v","type":"integer"},{"name":"y","type":"integer"},
... {"name":"v","type":"integer"},{"name":"x","type":"integer"},{"name":"z","type":"number"}],"primaryKey":["index"],"pandas_version":"0.20.
... 0"}, "data": [{"index":0,"v":0,"y":3,"v":0,"x":1,"z":null}]}', orient="table")
>>> f_(df_.shape[0], df_["x"].values, df_["y"].values, df_["z"].values)
>>> df_
   v  y  v  x   z
0  0  3  0  1 NaN

如果删除重复项,则该函数将按预期工作:

>>> df_.drop("v", axis="columns", inplace=True)
>>> f_(df_.shape[0], df_["x"].values, df_["y"].values, df_["z"].values)
>>> df_
   y  x    z
0  3  1  3.0

【问题讨论】:

    标签: python pandas numpy jit numba


    【解决方案1】:

    啊,那是因为在您的“失败案例”中,df["z"].values 返回存储在df'z' 列中的内容的副本。与numba函数无关:

    >>> import pandas as pd
    >>> import numpy as np
    >>> df = pd.DataFrame([[0, 3, 0, 1, np.nan]], columns=['v', 'y', 'v', 'x', 'z'])
    >>> np.shares_memory(df['z'].values, df['z'])
    False
    

    在“工作案例”中,它是'z' 列的视图:

    >>> df = pd.DataFrame([[0, 3, 1, np.nan]], columns=['v', 'y', 'x', 'z'])
    >>> np.shares_memory(df['z'].values, df['z'])
    True
    

    注意:这实际上很有趣,因为复制是在您执行 df['z'] 时制作的,而不是在您访问 .values 时制作的。

    这里的要点是,您不能期望索引 DataFrame 或访问 Series 的 .values 总是会返回一个视图。因此,就地更新列可能不会更改原始值。不仅重复的列名可能是一个问题。当values 属性返回一个副本并且当它返回一个视图时并不总是清晰的(pd.Series 除外,那么它总是一个视图)。但这些只是实现细节。因此,在这里依赖特定行为绝不是一个好主意。 .values 所做的唯一保证是它返回一个包含相同值的 numpy.ndarray

    不过,只需从函数中返回修改后的 z 列,就很容易避免该问题:

    import numba as nb
    import numpy as np
    import pandas as pd
    
    @nb.njit
    def f_(n, x, y, z):
        for i in range(n):
            z[i] = x[i] * y[i] 
        return z  # this is new
    

    然后将函数的结果赋给列:

    >>> df = pd.DataFrame([[0, 3, 0, 1, np.nan]], columns=['v', 'y', 'v', 'x', 'z'])
    >>> df['z'] = f_(df.shape[0], df["x"].values, df["y"].values, df["z"].values)
    >>> df
       v  y  v  x    z
    0  0  3  0  1  3.0
    
    >>> df = pd.DataFrame([[0, 3, 1, np.nan]], columns=['v', 'y', 'x', 'z'])
    >>> df['z'] = f_(df.shape[0], df["x"].values, df["y"].values, df["z"].values)
    >>> df
       v  y  x    z
    0  0  3  1  3.0
    

    如果您对当前在您的特定案例中发生的事情感兴趣(正如我提到的,我们在这里讨论的是实现细节,所以不要认为这是既定的。这只是它的实现方式现在)。如果你有一个 DataFrame,它会将具有相同 dtype 的列存储在一个多维 NumPy 数组中。如果您访问blocks 属性,则可以看到这一点(已弃用,因为内部存储可能在不久的将来发生变化):

    >>> df = pd.DataFrame([[0, 3, 0, 1, np.nan]], columns=['v', 'y', 'v', 'x', 'z'])
    >>> df.blocks
    {'float64':
         z
      0  NaN
     , 
     'int64':
         v  y  v  x
      0  0  3  0  1}
    

    通常,通过将列名转换为相应块的列索引,可以很容易地在该块中创建视图。但是,如果您有重复的列名,则不能保证访问任意列是一个视图。例如,如果你想访问'v',那么它必须用索引 0 和 2 索引 Int64 块:

    >>> df = pd.DataFrame([[0, 3, 0, 1, np.nan]], columns=['v', 'y', 'v', 'x', 'z'])
    >>> df['v']
       v  v
    0  0  0
    

    从技术上讲,可以将非重复列索引为视图(在这种情况下,即使是重复列,例如使用Int64Block[::2],但这是一个非常特殊的情况......)。如果有重复的列名,Pandas 会选择 always 返回副本的安全选项(如果您考虑一下,这是有道理的。为什么索引一列返回视图而另一列返回副本)。 DataFrame 的索引对重复列有一个 explicit check 并以不同方式处理它们(导致副本):

        def _getitem_column(self, key):
            """ return the actual column """
    
            # get column
            if self.columns.is_unique:
                return self._get_item_cache(key)
    
            # duplicate columns & possible reduce dimensionality
            result = self._constructor(self._data.get(key))
            if result.columns.is_unique:
                result = result[key]
    
        return result
    

    columns.is_unique 是这里的重要行。你的“正常情况”是True,但“失败情况”是“False”。

    【讨论】:

    • 什么时候.values 返回一个副本,什么时候返回一个视图并不总是清楚的。 ...(这是一个实现细节)。` - 这是非常有用的信息。我希望我可以投票两次。你知道这里使用两种不同实现的原因是什么吗?
    • @user10102398 感谢您的评论。也许我过于关注.values。实际复制发生在您索引 DataFrame 时,而不是在您获得 values 时。我相应地更新了答案(并包含了更多深入的信息)。我对 pandas 不太熟悉,但是当我研究这个问题时,我发现了几个(很多)pandas 问题,人们对索引或访问values 可以返回视图或副本感到惊讶。答案几乎总是:这些是实现细节(你真的不应该依赖它们)。
    猜你喜欢
    • 2023-03-11
    • 2019-06-29
    • 1970-01-01
    • 1970-01-01
    • 2021-01-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-03
    相关资源
    最近更新 更多