Numpy: эффективная замена значений в 2D-массиве с использованием словаря в качестве карты

у меня есть 2D numpy массив целых чисел, например:

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

и у меня есть словарь с целочисленными ключами и значениями, которые я хотел бы использовать, чтобы заменить значения a С новыми значениями. Словаре может выглядеть так:

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

я хочу заменить значения a Это соответствует ключу в d С соответствующим значением в 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 fancy. Но поскольку есть и отрицательные значения, это не сработает.

4 ответов


вот один из способов, если у вас есть небольшой словарь / мин и максимальные значения, это может быть более эффективным, вы работаете вокруг отрицательного индекса, добавив массив 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 в таблицу поиска, но если это значительно меньше, чем фактический массив, это может быть намного быстрее.


сделайте копию массива, затем выполните итерацию по элементам словаря, затем используйте логическое индексирование, чтобы назначить копии новые значения.

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 таким образом, давая нам эффективность просто индексирования без каких-либо смещений, необходимых для входящих входных массивов, что должно быть заметным улучшением здесь.

чтобы получить индексатор, который будет одноразовым использованием, поскольку словарь остается тем же, используй это -

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, do -

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

Numpy может создать векторизовать функции для выполнения операций отображения на массивах. Я не уверен, какой метод здесь будет иметь лучшую производительность, поэтому я приурочил свой подход, раз все. Я бы рекомендовал попробовать пару других подходов, если вы хотите выяснить, что имеет лучшую производительность.

# 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, с тем же словарем. Это заняло около четверти секунды для одного массива. Если вы не используете много этих массивов, возможно, не стоит оптимизировать, если это приемлемый уровень производительности.