jitted 函数的不一致行为
Inconsistent behavior of jitted function
我有一个像这样的非常简单的函数:
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
这在大多数情况下工作正常,但不知何故无法修改其他人的数据。
我仔细检查了事情并且:
- 我还没有确定任何可能导致此问题的数据点问题。
- 当我打印结果时,我看到数据已按预期修改。
- 如果我 return
z
函数中的数组按预期修改。
不幸的是,我无法将问题减少到最小的可重现案例。例如,删除不相关的列似乎 "fix" 无法减少的问题。
我是否以不适合的方式使用 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
啊,那是因为在你的 "failing case" 中 df["z"].values
return 是 'z'
中存储内容的 副本 df
列。与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
虽然在 "working case" 中是 '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 或访问系列的 .values
总是 return 视图。因此就地更新列 可能不会 更改原始值。不仅重复的列名可能是个问题。当 属性 values
return 是一个副本并且当它 return 是一个视图时,视图并不总是清晰的(除了 pd.Series
然后它总是一个视图)。但这些只是实现细节。因此,在这里依赖特定行为绝不是一个好主意。 .values
所做的唯一保证是它 return 是一个包含相同值的 numpy.ndarray
。
然而,通过简单地从函数 returning 修改后的 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 return 如果有重复的列名则复制一份(如果你考虑一下就有意义了。为什么要索引一列 return 一个视图和另一个 return 副本)。 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
是这里的重要一行。 "normal case" 是 True
,"failing case" 是 "False"。
我有一个像这样的非常简单的函数:
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
这在大多数情况下工作正常,但不知何故无法修改其他人的数据。
我仔细检查了事情并且:
- 我还没有确定任何可能导致此问题的数据点问题。
- 当我打印结果时,我看到数据已按预期修改。
- 如果我 return
z
函数中的数组按预期修改。
不幸的是,我无法将问题减少到最小的可重现案例。例如,删除不相关的列似乎 "fix" 无法减少的问题。
我是否以不适合的方式使用 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
啊,那是因为在你的 "failing case" 中 df["z"].values
return 是 'z'
中存储内容的 副本 df
列。与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
虽然在 "working case" 中是 '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 或访问系列的 .values
总是 return 视图。因此就地更新列 可能不会 更改原始值。不仅重复的列名可能是个问题。当 属性 values
return 是一个副本并且当它 return 是一个视图时,视图并不总是清晰的(除了 pd.Series
然后它总是一个视图)。但这些只是实现细节。因此,在这里依赖特定行为绝不是一个好主意。 .values
所做的唯一保证是它 return 是一个包含相同值的 numpy.ndarray
。
然而,通过简单地从函数 returning 修改后的 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 return 如果有重复的列名则复制一份(如果你考虑一下就有意义了。为什么要索引一列 return 一个视图和另一个 return 副本)。 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
是这里的重要一行。 "normal case" 是 True
,"failing case" 是 "False"。