【问题标题】:Numpy: Replacing values in a 2D array efficiently using a dictionary as a mapNumpy:使用字典作为地图有效地替换二维数组中的值
【发布时间】: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 请在此处查看我更新的解决方案以利用一对一映射案例 - stackoverflow.com/a/46870227 应该非常有效。

标签: python arrays numpy dictionary


【解决方案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://stackoverflow.com/a/46868897/2135504,但由于

    • 使用 np.empty_like() 代替 np.copy()
    • 使用 np.unique(src) 代替 new_by_old.keys()

    【讨论】:

      猜你喜欢
      • 2020-06-07
      • 2015-07-13
      • 1970-01-01
      • 1970-01-01
      • 2021-06-24
      • 1970-01-01
      • 2020-08-20
      • 1970-01-01
      相关资源
      最近更新 更多