关于python:numpy:使用字典作为映射有效地替换二维数组中的值

Numpy: Replacing values in a 2D array efficiently using a dictionary as a map

我有一个类似这样的二维整数数组:

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

我有一个字典,里面有整数键和值,我想用新值替换a的值。听写可能如下所示:

1
d = {0: 1, 1: 2, 2: 3, 3: 4, -1: 0, 255: 0}

我想用d中对应的值替换与d中键匹配的a的值。换句话说,d定义了a中旧(当前)值和新(期望)值之间的映射。上面的玩具例子的结果是:

1
2
3
a_new = np.array([[  4,   1,   3,   0],
                  [  2,   0,   2,   3],
                  [  1,   4,   3,   3]])

实现这一点的有效方法是什么?

这是一个玩具例子,但实际上数组将是大的,它的形状将如(1024, 2048),字典将有几十个元素的顺序(在我的例子中是34),虽然键是整数,但它们不一定都是连续的,它们可以是负的(就像上面的例子)。

我需要在数十万个这样的数组上执行这个替换,所以它需要很快。然而,字典是预先知道的,并且保持不变,因此渐进地,任何时候用来修改字典或将其转换为更合适的数据结构都不重要。

我目前正在两个嵌套的for循环(在a的行和列上)中循环数组项,但必须有更好的方法。

如果映射不包含负值(如示例中的-1),那么我只需要从字典中创建一个列表或数组,其中键是数组索引,然后将其用于一个有效的nummy-fashing索引例程。但既然也有负值,这就行不通了。


这里有一种方法,如果您有一个小的dictionary/min和max值,这可能更有效,您可以通过添加数组min来解决负索引:

1
2
3
4
5
6
7
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循环移动到查找表中,但如果它明显小于实际数组,则速度可能会更快。


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

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


本文解决了数组和字典键之间的一对一映射问题。这一想法与@Andy Hayden's smart solution中提出的想法类似,但我们将创建一个更大的数组,其中包含Python's negative indexing,从而使我们能够简单地索引,而不需要对输入数组进行任何补偿,这应该是显著的改进。

要获取索引器,这是由于字典保持不变而一次性使用的,请使用此-

1
2
3
4
5
6
7
8
9
10
11
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,应该-

1
out = val_arr[a]

样品运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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]])

平铺样本数据的运行时测试-

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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


numpy可以创建用于对数组执行映射操作的向量化函数。我不确定这里的哪个方法会有最好的性能,所以我已经用timeit对我的方法进行了计时。如果你想知道什么性能最好,我建议尝试其他一些方法。

1
2
3
4
5
6
7
8
9
# 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))

您可以通过执行以下操作来计时:

1
2
3
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,使用相同的字典。一个阵列大约用了四分之一秒。除非您运行了许多这样的数组,否则如果这是一个可接受的性能水平,那么就不值得进行优化。