基于字典,高效地在NumPy/Python数组中替换元素

18

首先,如果这个问题已经在其他地方得到了解答,则向您道歉。我所能找到的都是关于替换给定值元素而不是多个值元素的问题。

背景

我有几千个大型np.array,就像这样:

# generate dummy data
input_array = np.zeros((100,100))
input_array[0:10,0:10] = 1
input_array[20:56, 21:43] = 5
input_array[34:43, 70:89] = 8
在这些数组中,我想根据一个字典替换值:
mapping = {1:2, 5:3, 8:6}

方法

目前,我正在使用简单的循环结构,与花式索引相结合:

output_array = np.zeros_like(input_array)

for key in mapping:
    output_array[input_array==key] = mapping[key]

问题

我的数组大小为2000乘以2000,字典大约有1000个条目,所以这些循环需要花费很长时间。

问题

是否存在一个函数,它简单地接受一个数组和一个映射(以字典或类似形式表示),并输出更改后的值?

非常感谢您的帮助!

更新:

解决方案:

我在Ipython中使用以下命令测试了各个解决方案:

%% timeit -r 10 -n 10

输入数据

import numpy as np
np.random.seed(123)

sources = range(100)
outs = [a for a in range(100)]
np.random.shuffle(outs)
mapping = {sources[a]:outs[a] for a in(range(len(sources)))}

对于每一个解决方案:

np.random.seed(123)
input_array = np.random.randint(0,100, (1000,1000))

Divakar,方法3:

%%timeit -r 10 -n 10
k = np.array(list(mapping.keys()))
v = np.array(list(mapping.values()))

mapping_ar = np.zeros(k.max()+1,dtype=v.dtype) #k,v from approach #1
mapping_ar[k] = v
out = mapping_ar[input_array]

5.01 ms ± 641 µs per loop (mean ± std. dev. of 10 runs, 10 loops each)

Divakar,第二种方法:

%%timeit -r 10 -n 10
k = np.array(list(mapping.keys()))
v = np.array(list(mapping.values()))

sidx = k.argsort() #k,v from approach #1

k = k[sidx]
v = v[sidx]

idx = np.searchsorted(k,input_array.ravel()).reshape(input_array.shape)
idx[idx==len(k)] = 0
mask = k[idx] == input_array
out = np.where(mask, v[idx], 0)

56.9 ms ± 609 µs per loop (mean ± std. dev. of 10 runs, 10 loops each)

divakar,方法1:

%%timeit -r 10 -n 10

k = np.array(list(mapping.keys()))
v = np.array(list(mapping.values()))

out = np.zeros_like(input_array)
for key,val in zip(k,v):
    out[input_array==key] = val

113 ms ± 6.2 ms per loop (mean ± std. dev. of 10 runs, 10 loops each)

eelco:

%%timeit -r 10 -n 10
output_array = npi.remap(input_array.flatten(), list(mapping.keys()), list(mapping.values())).reshape(input_array.shape)

143 ms ± 4.47 ms per loop (mean ± std. dev. of 10 runs, 10 loops each)

yatu

%%timeit -r 10 -n 10

keys, choices = list(zip(*mapping.items()))
# [(1, 5, 8), (2, 3, 6)]
conds = np.array(keys)[:,None,None]  == input_array
np.select(conds, choices)

157 ms ± 5 ms per loop (mean ± std. dev. of 10 runs, 10 loops each)

原始、循环的方法:

%%timeit -r 10 -n 10
output_array = np.zeros_like(input_array)

for key in mapping:
    output_array[input_array==key] = mapping[key]

187 ms ± 6.44 ms per loop (mean ± std. dev. of 10 runs, 10 loops each)

感谢您的超快帮助!


2
我认为这是同一个问题。最佳答案可能是这个。 - Brenlla
如下所述:第一次调用list是一个错误;我认为没有它会快得多。 - Eelco Hoogendoorn
请注意,这些解决方案假定所有源值都大于或等于0。此外,它假定源不太稀疏,因此具有最大值长度的数组可以适合内存。 - Louis Yang
4个回答

11

方法1:使用数组数据的循环法

一种方法是将键和值提取到数组中,然后使用类似的循环-

k = np.array(list(mapping.keys()))
v = np.array(list(mapping.values()))

out = np.zeros_like(input_array)
for key,val in zip(k,v):
    out[input_array==key] = val

与原始版本相比,这个版本的优点在于数组数据的空间局部性,可用于迭代中的高效数据获取。

另外,由于您提到了“数千个大型 np.arrays”,因此,如果“映射”字典保持不变,则获取数组版本 - kv 的步骤将是一次性的设置过程。

方法二:使用searchsorted进行矢量化

可以建议使用np.searchsorted进行矢量化处理 -

sidx = k.argsort() #k,v from approach #1

k = k[sidx]
v = v[sidx]

idx = np.searchsorted(k,input_array.ravel()).reshape(input_array.shape)
idx[idx==len(k)] = 0
mask = k[idx] == input_array
out = np.where(mask, v[idx], 0)

方法三:使用映射数组的矢量化方法来处理整数键

可以建议使用一个映射数组来处理整数键的矢量化方法,当按输入数组索引时,它会直接将我们带到最终输出。

mapping_ar = np.zeros(k.max()+1,dtype=v.dtype) #k,v from approach #1
mapping_ar[k] = v
out = mapping_ar[input_array]

第三种方法假设input_array是一个非负整数数组,并且k包含了input_arr的所有值。第二个问题可以通过将mapping_ar = np.zeros(k.max()+1,dtype=v.dtype)替换为mapping_ar = np.arange(input_arr.max()+1)来解决,但如果input_arr具有大的值,则这种方法不会很有效。 - bb1
在第二种方法中,最后一行应该被替换为 out = np.where(mask, v[idx], input_array) - bb1
mask = k[idx] == maxes 类型错误:只有整数标量数组可以转换为标量索引(maxes来自nanargmax) - Flash Thunder

4
我认为Divakar #3方法假设映射字典覆盖了目标数组中的所有值(或至少是最大值)。否则,为了避免索引超出范围的错误,您必须将该行替换为: mapping_ar = np.zeros(array.max()+1,dtype=v.dtype) 这会增加相当大的开销。

2

numpy_indexed库(免责声明:我是它的作者)提供了实现这种操作的高效向量化功能:

Original Answer翻译成"最初的回答"

import numpy_indexed as npi
output_array = npi.remap(input_array.flatten(), list(mapping.keys()), list(mapping.values())).reshape(input_array.shape)

注意:我没有测试过它,但应该可以沿这个方向工作。对于大输入和许多映射项,效率应该很高,我想象类似于divakars的方法2;不像他的方法3那样快。但是,这个解决方案旨在更通用; 它也将适用于不是正整数甚至nd-arrays的输入(例如用其他颜色替换图像中的颜色等)。"Original Answer"翻译成"最初的回答"

谢谢!我必须稍微调整你的代码,以适应Python 3中的mapping.values()list(mapping_values) - warped
把列表放在输入周围而不是值上,这是一个OOPS。实际上你需要后者而不是前者,因为前者会没有任何好处地拖慢速度。我更新了我的回答。 - Eelco Hoogendoorn
好的,我的错。已经使用你的编辑更新了帖子。性能提高了240毫秒 :) - warped
有趣的是它仍然比divakar方法1慢;您是在使用具有1000个条目的映射进行基准测试,还是像您示例中的3个条目映射这样更简单的问题? - Eelco Hoogendoorn
测试条件分别位于解决方案和输入数据标题下。为简单起见,在10次运行中,我使用相同的1000x1000数组,每个循环有10个循环。 - warped
哦,你确实提到了;我的错;100个条目。哇,没想到在那种情况下直接循环会更快...应该回到那段代码并进行分析,看看是否有任何明显的瓶颈。即使是1000个条目,朴素的方法也会落后。 - Eelco Hoogendoorn

1
考虑到您正在使用numpy数组,我建议您也使用numpy进行映射。以下是使用np.select的向量化方法:
mapping = {1:2, 5:3, 8:6}
keys, choices = list(zip(*mapping.items()))
# [(1, 5, 8), (2, 3, 6)]
# we can use broadcasting to obtain a 3x100x100
# array to use as condlist
conds = np.array(keys)[:,None,None]  == input_array
# use conds as arrays of conditions and the values 
# as choices
np.select(conds, choices)

array([[2, 2, 2, ..., 0, 0, 0],
       [2, 2, 2, ..., 0, 0, 0],
       [2, 2, 2, ..., 0, 0, 0],
       ...,
       [0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0],
       [0, 0, 0, ..., 0, 0, 0]])

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