Numpy:使用字典作为映射,在二维数组中高效地替换值

15

我有一个类似以下所示的2D Numpy整数数组:

a = np.array([[  3,   0,   2,  -1],
              [  1, 255,   1,   2],
              [  0,   3,   2,   2]])

我有一个具有整数键和值的字典,我想使用它来替换a的值为新值。这个字典可能看起来像这样:

and I have a dictionary with integer keys and values that I would like to use to replace the values of a with new values. The dict might look like this:

d = {0: 1, 1: 2, 2: 3, 3: 4, -1: 0, 255: 0}
我想用d中与a中的键匹配的相应值替换a的值。换句话说,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 fancy indexing 程序进行操作。但既然还有负值,所以这种方法行不通。

2
我非常喜欢这个问题。有两个想法:(1)像Andy在下面建议的那样,用一个聪明的NumPy数组来替换字典(你还可以通过函数和索引器构造索引器和/或运行原始数据值,然后再进行索引),或者(2)考虑使用Pandas Series/DataFrame,它具有一些不错的替换方法,可能足够快速。 - MrDrFenner
1
可能是快速替换numpy数组中的值的重复问题(在我回答后发现)。 - wwii
@wwii 我对那些数字并不是非常有信心,如果它是一个小字典的话,那么可以肯定,但如果它只有几倍的元素,那么它会慢得多。无论如何,我认为我们的两个答案都是要尝试的解决方案(根据你的字典/数据,其中一个将更快/更好) :) - Andy Hayden
@wwii 不一定需要更改数组的所有值。到目前为止,我采用的方法是,字典不一定包含所有唯一数组元素的映射,但当然我可以始终添加身份映射(键=值)以保留所有应保持不变的唯一数组元素,并且具有“完整”的字典如果这对某些方法有用的话。一个重要的事情是:我预先知道映射,因此字典创建一次后就保持不变,并用于处理数千个大型二维数组。因此,修改字典的时间无关紧要。 - Alex
1
{btsdaf} - Divakar
显示剩余5条评论
5个回答

5

复制该数组,然后迭代字典项,使用布尔索引将新值分配给副本。

import numpy as np
b = np.copy(a)
for old, new in d.items():
    b[a == old] = new

5

如果你有一个小的字典/最小值和最大值,这是一种方法,可能更加高效。通过添加数组最小值来解决负索引问题:

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]])

注意:这将循环移动到查找表中,但如果查找表明显比实际数组小,则可能会更快。


你的解决方案的好处在于我只需要创建这个索引器一次,所以它的复杂度并不重要,即使字典很大(实际上它并不大)。 - Alex
1
{btsdaf} - Andy Hayden
如果 a[0,0]5,这个代码还能正常工作吗?换句话说,如果在 d 的键和 a 中的唯一值之间没有一对一的对应关系,它还能正常工作吗? - wwii
2
这是我情况下最好的方法:由于字典保持不变且所有可能的数组值都已知,因此只需要创建一次索引器,就可以用来处理大量的数组。如果索引器只需要创建一次,那么这种方法比@wwii提出的方法快大约6倍。如果每个要处理的数组都需要新建索引器,那么它就不会更快了,我猜测。 - Alex
如果你想在字典中没有对应值的情况下保留原始数组的值,可以将 d.get(i, -1) 替换为 d.get(i, i) - Steven Walton
显示剩余4条评论

3
这篇文章解决了数组和字典键之间的一对一映射情况。这个想法与@Andy Hayden 的聪明解决方案类似,但我们将创建一个更大的数组,其中包括Python 的负索引,从而使我们能够直接索引而不需要任何偏移来获取输入数组,这应该是非常明显的改进。
要获得索引器,这将是一次性使用,因为字典保持不变,请使用以下方法 -
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()) for Python 3,对于 k 也是一样。 - wwii
如果 a[0,0]5,这个代码还能正常工作吗?换句话说,如果在 d 的键和 a 中的唯一值之间没有一对一的对应关系,它还能正常工作吗? - wwii
利用负索引是个好主意!我会试一下的。 - Alex
@Alex,你有试过发布的建议吗? - Divakar

0
Numpy可以创建向量化函数,用于在数组上执行映射操作。我不确定哪种方法会有最佳性能,所以我使用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)的numpy数组上尝试了一下,其中包含从-10到10的随机数,使用了您相同的字典。对于单个数组,大约需要四分之一秒的时间。除非您运行了很多这些数组,否则如果这是可接受的性能水平,可能不值得优化。


vectorize 的文档说:“vectorize 函数主要是为了方便而提供的,而不是为了性能。实现本质上是一个 for 循环。”,但我会尝试一下! - Alex
是的,在测试后,安迪使用索引器的方法表现更好。他的方法只用了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)])”,以保留原始数组的值,对于没有相应字典键的情况。 - Steven Walton

0

另一个选项,还没有进行基准测试:

    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://dev59.com/vFYN5IYBdhLWcg3wza54#46868897类似,但由于以下原因应该会更快:

  • 使用np.empty_like()而不是np.copy()
  • 使用np.unique(src)而不是new_by_old.keys()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接