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:使用字典作为地图有效地替换二维数组中的值的主要内容,如果未能解决你的问题,请参考以下文章

如何有效地计算 numpy 二维数组的块均值(不规则块)?

替换二维numpy数组中字符的所有实例[重复]

Numpy 查找(地图或点)

如何有效地找到 NumPy 中平滑多维数组的局部最小值?

有效地计算numpy数组中的零元素?

用NumPy数组中的```[i,i,i]```快速替换元素i的pythonic方法?