Numpy:使用字典作为地图有效地替换二维数组中的值
Posted
技术标签:
【中文标题】Numpy:使用字典作为地图有效地替换二维数组中的值【英文标题】:Numpy: Replacing values in a 2D array efficiently using a dictionary as a map 【发布时间】:2018-04-02 18:59:28 【问题描述】:我有一个二维 Numpy 整数数组,如下所示:
a = np.array([[ 3, 0, 2, -1],
[ 1, 255, 1, 2],
[ 0, 3, 2, 2]])
我有一个包含整数键和值的字典,我想用它来用新值替换 a
的值。 dict 可能如下所示:
d = 0: 1, 1: 2, 2: 3, 3: 4, -1: 0, 255: 0
我想将与d
中的键匹配的a
的值替换为d
中的对应值。换句话说,d
定义了a
中旧(当前)和新(期望)值之间的映射。上面的玩具示例的结果是这样的:
a_new = np.array([[ 4, 1, 3, 0],
[ 2, 0, 2, 3],
[ 1, 4, 3, 3]])
什么是实现这一点的有效方法?
这是一个玩具示例,但实际上数组会很大,它的形状将是例如(1024, 2048)
,字典将有几十个元素(在我的例子中是 34 个),虽然键是整数,但它们不一定都是连续的,它们可以是负数(如上面的示例)。
我需要在数十万个这样的阵列上执行此替换,因此它需要快速。但是,字典是预先知道的并且保持不变,因此渐近地,任何时间用于修改字典或将其转换为更合适的数据结构都无关紧要。
我目前正在两个嵌套的for
循环中循环数组条目(在a
的行和列上),但必须有更好的方法。
如果映射不包含负值(例如示例中的 -1),我只需从字典中创建一个列表或数组,其中键是数组索引,然后将其用于高效的 Numpy花哨的索引例程。但是由于也有负值,所以这行不通。
【问题讨论】:
我非常喜欢这个问题。两个想法:(1) 用一个聪明的 NumPy 数组替换 dict,正如 Andy 在下面建议的那样(还有其他一些方法可以构造索引器和/或通过函数然后是索引器运行原始数据值)或 (2) 考虑使用一个 Pandas Series/DataFrame,它有一些不错的替换方法,可能足够快。 好点,我会研究 Pandas 数据结构! Fast replacement of values in a numpy array 的可能重复项...(在我回答后发现)。 @wwii 我不太相信那里的数字,我认为如果它肯定是一个小字典,但如果它只有几倍的元素,它会慢得多。无论如何,我认为我们的两个答案是尝试的两种解决方案(取决于您的 dict/data 一个会更快/最好):) @Alex 请在此处查看我更新的解决方案以利用一对一映射案例 - ***.com/a/46870227 应该非常有效。 【参考方案1】:这是一种方法,如果您有一个小的字典/最小值和最大值,这可能更有效,您可以通过添加数组 min 来解决负索引:
In [11]: indexer = np.array([d.get(i, -1) for i in range(a.min(), a.max() + 1)])
In [12]: indexer[(a - a.min())]
Out[12]:
array([[4, 1, 3, 0],
[2, 0, 2, 3],
[1, 4, 3, 3]])
注意:这会将 for 循环移动到查找表,但如果它比实际数组小很多,这可能会快很多。
【讨论】:
这基本上允许您从问题中删除条件“如果地图不包含负值”。 是的,行得通!我猜在复杂性方面这类似于遍历所有字典项目并执行a[a == key] = value
?有人在这里的另一个答案中提出了建议,但随后奇怪地删除了它。
您的解决方案的好处是我只需要创建一次这个索引器,因此它的复杂性并不重要,即使字典很大(它不是)。跨度>
@Alex 我认为另一个的复杂性是“相似的”(因为它最适合小型词典),我怀疑两者的性能都可以,但我怀疑这会稍微好一些对于较大的数组,因为它只需要 3 遍。
在我的情况下这是最好的方法:由于字典保持不变并且所有可能的数组值都是预先知道的,因此只需创建一次索引器,然后就可以使用它来处理大量数字的数组。如果只需要创建一次索引器,这个方法比@wwii 提出的方法快大约6倍。如果需要为每个要处理的数组重新创建索引器,那么我猜它不会更快。【参考方案2】:
制作数组的副本,然后遍历字典项,然后使用布尔索引将新值分配给副本。
import numpy as np
b = np.copy(a)
for old, new in d.items():
b[a == old] = new
【讨论】:
【参考方案3】:这篇文章解决了数组和字典键之间的一对一映射情况。这个想法类似于@Andy Hayden's smart solution
中提出的想法,但我们将创建一个更大的数组,其中包含Python's negative indexing
,从而为我们提供简单索引的效率,而无需对传入的输入数组进行任何偏移,这应该是这里的显着改进。
要获取索引器,这将是一次性使用,因为字典保持不变,使用这个 -
def getval_array(d):
v = np.array(list(d.values()))
k = np.array(list(d.keys()))
maxv = k.max()
minv = k.min()
n = maxv - minv + 1
val = np.empty(n,dtype=v.dtype)
val[k] = v
return val
val_arr = getval_array(d)
要获得最终替换,只需索引。所以,对于输入数组a
,做-
out = val_arr[a]
示例运行 -
In [8]: a = np.array([[ 3, 0, 2, -1],
...: [ 1, 255, 1, -16],
...: [ 0, 3, 2, 2]])
...:
...: d = 0: 1, 1: 2, 2: 3, 3: 4, -1: 0, 255: 0, -16:5
...:
In [9]: val_arr = getval_array(d) # one-time operation
In [10]: val_arr[a]
Out[10]:
array([[4, 1, 3, 0],
[2, 0, 2, 5],
[1, 4, 3, 3]])
平铺样本数据的运行时测试 -
In [141]: a = np.array([[ 3, 0, 2, -1],
...: [ 1, 255, 1, -16],
...: [ 0, 3, 2, 2]])
...:
...: d = 0: 1, 1: 2, 2: 3, 3: 4, -1: 10, 255: 89, -16:5
...:
In [142]: a = np.random.choice(a.ravel(), 1024*2048).reshape(1024,2048)
# @Andy Hayden's soln
In [143]: indexer = np.array([d.get(i, -1) for i in range(a.min(), a.max() + 1)])
In [144]: %timeit indexer[(a - a.min())]
100 loops, best of 3: 8.34 ms per loop
# Proposed in this post
In [145]: val_arr = getval_array(d)
In [146]: %timeit val_arr[a]
100 loops, best of 3: 2.69 ms per loop
【讨论】:
v = list(d.values())
用于 Python 3,k
相同。
如果a[0,0]
是5
,这会起作用吗?换句话说,如果d
的键和a
的唯一值之间没有1:1 的对应关系,它会起作用吗?
利用负索引的好主意!我会试试这个。
@Alex 那么,你能尝试一下发布的建议吗?【参考方案4】:
Numpy 可以创建vectorized functions 用于对数组执行映射操作。我不确定这里的哪种方法性能最好,所以我用 timeit 对我的方法进行了计时。如果您想找出性能最佳的方法,我建议您尝试其他几种方法。
# Function to be vectorized
def map_func(val, dictionary):
return dictionary[val] if val in dictionary else val
# Vectorize map_func
vfunc = np.vectorize(map_func)
# Run
print(vfunc(a, d))
你可以这样做:
from timeit import Timer
t = Timer('vfunc(a, d)', 'from __main__ import a, d, vfunc')
print(t.timeit(number=1000))
我对这种方法的结果约为 0.014 秒。
编辑:为了好玩,我在(1024, 2048)
size numpy array of random numbers from -10 to 10 上尝试了这个,使用相同的字典。单个阵列大约需要四分之一秒。除非您运行大量此类阵列,否则如果这是可以接受的性能水平,则可能不值得优化。
【讨论】:
vectorize
的文档说:“提供 vectorize 函数主要是为了方便,而不是为了性能。实现本质上是一个 for 循环。”但我会尝试一下!
是的,经过测试,Andy 使用索引器的方法表现得更好。我用他的方法得到了 0.014 秒,而使用矢量化得到了 0.27 秒。唯一的调整是,由于我的测试数组包含字典中不存在的值,我将 indexer = np.array([d.get(i, -1) for i in range(a.min(), a.max() + 1)])
更改为 indexer = np.array([d.get(i, i) for i in range(a.min(), a.max() + 1)])
以在没有对应字典键的情况下保留原始数组的值。【参考方案5】:
另一种选择,尚未对其进行基准测试:
def replace_values(src: np.ndarray, new_by_old: Dict[int,int]) -> np.ndarray:
dst = np.empty_like(src)
for x in np.unique(src):
dst[src==x] = new_by_old[x]
return dst
这类似于https://***.com/a/46868897/2135504,但由于
使用 np.empty_like() 代替 np.copy() 使用 np.unique(src) 代替 new_by_old.keys()【讨论】:
以上是关于Numpy:使用字典作为地图有效地替换二维数组中的值的主要内容,如果未能解决你的问题,请参考以下文章