2010-08-04 3 views
30

У меня есть очень большой Numpy массив (содержащий до миллиона элементов), как показано ниже:Быстрая замена значений в Numpy массиве

[ 0 1 6 5 1 2 7 6 2 3 8 7 3 4 9 8 5 6 11 10 6 7 12 11 7 
    8 13 12 8 9 14 13 10 11 16 15 11 12 17 16 12 13 18 17 13 14 19 18 15 16 
21 20 16 17 22 21 17 18 23 22 18 19 24 23] 

и небольшой словарь карты для замены некоторых элементов в вышеуказанная матрица

{4: 0, 9: 5, 14: 10, 19: 15, 20: 0, 21: 1, 22: 2, 23: 3, 24: 0} 

Я бы хотел заменить некоторые элементы в соответствии с приведенной выше картой. Массив numpy действительно большой, и только небольшое подмножество элементов (встречающихся как ключи в словаре) будет заменено соответствующими значениями. Каков самый быстрый способ сделать это?

ответ

28

Я считаю, что есть еще более эффективный метод, но сейчас, попробуйте

from numpy import copy 

newArray = copy(theArray) 
for k, v in d.iteritems(): newArray[theArray==k] = v 

Microbenchmark и тест на корректность:

#!/usr/bin/env python2.7 

from numpy import copy, random, arange 

random.seed(0) 
data = random.randint(30, size=10**5) 

d = {4: 0, 9: 5, 14: 10, 19: 15, 20: 0, 21: 1, 22: 2, 23: 3, 24: 0} 
dk = d.keys() 
dv = d.values() 

def f1(a, d): 
    b = copy(a) 
    for k, v in d.iteritems(): 
     b[a==k] = v 
    return b 

def f2(a, d): 
    for i in xrange(len(a)): 
     a[i] = d.get(a[i], a[i]) 
    return a 

def f3(a, dk, dv): 
    mp = arange(0, max(a)+1) 
    mp[dk] = dv 
    return mp[a] 


a = copy(data) 
res = f2(a, d) 

assert (f1(data, d) == res).all() 
assert (f3(data, dk, dv) == res).all() 

Результат:

$ python2.7 -m timeit -s 'from w import f1,f3,data,d,dk,dv' 'f1(data,d)' 
100 loops, best of 3: 6.15 msec per loop 

$ python2.7 -m timeit -s 'from w import f1,f3,data,d,dk,dv' 'f3(data,dk,dv)' 
100 loops, best of 3: 19.6 msec per loop 
+3

'numpy.place' Я думаю ... – katrielalex

+0

Итерации, подобные' for k in d', сделают это как можно быстрее' – jamylak

+0

Проголосовать против 'numpy.place', как упоминалось @katrielalex, поскольку он просто потратил впустую около двадцати тридцать часов моего времени, будучи багги; по-видимому, его использование, соответственно, обескуражено. «Обычно я предлагаю использовать« np.copyto »или (в данном случае) логическую индексацию, чтобы добиться того же и избежать« np.place »или« np.putmask ». Я понимаю, что в некоторых случаях эти функции не совсем 1: 1 заменяется ими ». FWIW У меня не было этой ошибки, но еще одна, где она просто не работала. – ijoseph

0

Ну, вам нужно сделать один проход через theArray, и для каждого элемента замените его, если он находится в словаре.

for i in xrange(len(theArray)): 
    if foo[ i ] in dict: 
     foo[ i ] = dict[ foo[ i ] ] 
+0

Было бы лучше поставить LEN (theArray) в переменный и использовать xrange. – fuwaneko

+0

@fuw: Да xrange, но помещать 'len (theArray)' в переменную не поможет, потому что итератор оценивается один раз. – kennytm

+0

Py3k 'range' - это генератор. – katrielalex

0
for i in xrange(len(the_array)): 
    the_array[i] = the_dict.get(the_array[i], the_array[i]) 
16

Предполагая, что значения от 0 до некоторого максимального целого числа, можно было бы осуществить быстрые замены, используя Numpy-массив в качестве int->int Dict, как ниже

mp = numpy.arange(0,max(data)+1) 
mp[replace.keys()] = replace.values() 
data = mp[data] 

, где первого

data = [ 0 1 6 5 1 2 7 6 2 3 8 7 3 4 9 8 5 6 11 10 6 7 12 11 7 
    8 13 12 8 9 14 13 10 11 16 15 11 12 17 16 12 13 18 17 13 14 19 18 15 16 
21 20 16 17 22 21 17 18 23 22 18 19 24 23] 

и заменой на

replace = {4: 0, 9: 5, 14: 10, 19: 15, 20: 0, 21: 1, 22: 2, 23: 3, 24: 0} 

получаем

data = [ 0 1 6 5 1 2 7 6 2 3 8 7 3 0 5 8 5 6 11 10 6 7 12 11 7 
    8 13 12 8 5 10 13 10 11 16 15 11 12 17 16 12 13 18 17 13 10 15 18 15 16 
    1 0 16 17 2 1 17 18 3 2 18 15 0 3] 
+0

Также обратите внимание на функцию «digitize», показанную в принятом ответе на этот вопрос: http://stackoverflow.com/questions/13572448/change-values-in-a-numpy-array –

3

Еще более общий способ для достижения этой цели является функция векторизации:

import numpy as np 

data = np.array([0, 1, 6, 5, 1, 2, 7, 6, 2, 3, 8, 7, 3, 4, 9, 8, 5, 6, 11, 10, 6, 7, 12, 11, 7, 8, 13, 12, 8, 9, 14, 13, 10, 11, 16, 15, 11, 12, 17, 16, 12, 13, 18, 17, 13, 14, 19, 18, 15, 16, 21, 20, 16, 17, 22, 21, 17, 18, 23, 22, 18, 19, 24, 23]) 
mapper_dict = {4: 0, 9: 5, 14: 10, 19: 15, 20: 0, 21: 1, 22: 2, 23: 3, 24: 0} 

def mp(entry): 
    return mapper_dict[entry] if entry in mapper_dict else entry 
mp = np.vectorize(mp) 

print mp(data) 
+2

или просто 'return mapper_dict.get (запись, запись) ' – grisaitis

2

Ни одно решение не было передано еще без петли питона на массив (за исключением Celil один, который, однако, считать цифры "маленький"), так вот альтернатива:

def replace(arr, rep_dict): 
    """Assumes all elements of "arr" are keys of rep_dict""" 

    # Removing the explicit "list" breaks python3 
    rep_keys, rep_vals = array(list(zip(*sorted(rep_dict.items())))) 

    idces = digitize(arr, rep_keys, right=True) 
    # Notice rep_keys[digitize(arr, rep_keys, right=True)] == arr 

    return rep_vals[idces] 

путь "idces" создается происходит от here.

2

Я протестированные некоторые решения, и результат безапелляционно:

import timeit 
import numpy as np 

array = 2 * np.round(np.random.uniform(0,10000,300000)).astype(int) 
from_values = np.unique(array) # pair values from 0 to 2000 
to_values = np.arange(from_values.size) # all values from 0 to 1000 
d = dict(zip(from_values, to_values)) 

def method_for_loop(): 
    out = array.copy() 
    for from_value, to_value in zip(from_values, to_values) : 
     out[out == from_value] = to_value 
    print('Check method_for_loop :', np.all(out == array/2)) # Just checking 
print('Time method_for_loop :', timeit.timeit(method_for_loop, number = 1)) 

def method_list_comprehension(): 
    out = [d[i] for i in array] 
    print('Check method_list_comprehension :', np.all(out == array/2)) # Just checking 
print('Time method_list_comprehension :', timeit.timeit(method_list_comprehension, number = 1)) 

def method_bruteforce(): 
    idx = np.nonzero(from_values == array[:,None])[1] 
    out = to_values[idx] 
    print('Check method_bruteforce :', np.all(out == array/2)) # Just checking 
print('Time method_bruteforce :', timeit.timeit(method_bruteforce, number = 1)) 

def method_searchsort(): 
    sort_idx = np.argsort(from_values) 
    idx = np.searchsorted(from_values,array,sorter = sort_idx) 
    out = to_values[sort_idx][idx] 
    print('Check method_searchsort :', np.all(out == array/2)) # Just checking 
print('Time method_searchsort :', timeit.timeit(method_searchsort, number = 1)) 

И я получил следующие результаты:

Check method_for_loop : True 
Time method_for_loop : 2.6411612760275602 

Check method_list_comprehension : True 
Time method_list_comprehension : 0.07994363596662879 

Check method_bruteforce : True 
Time method_bruteforce : 11.960559037979692 

Check method_searchsort : True 
Time method_searchsort : 0.03770717792212963 

Метод «searchsort» почти сто раз быстрее чем цикл «для», и около 3600 раз быстрее, чем метод numpy bruteforce. Метод понимания списка также является очень хорошим компромиссом между простотой кода и скоростью.

+2

Ahem ... сто раз? –

0

Pythonic способ без необходимости данных, чтобы быть целым числом, может быть даже строки:

from scipy.stats import rankdata 
import numpy as np 

data = np.random.rand(100000) 
replace = {data[0]: 1, data[5]: 8, data[8]: 10} 

arr = np.vstack((replace.keys(), replace.values())).transpose() 
arr = arr[arr[:,1].argsort()] 

unique = np.unique(data) 
mp = np.vstack((unique, unique)).transpose() 
mp[np.in1d(mp[:,0], arr),1] = arr[:,1] 
data = mp[rankdata(data, 'dense')-1][:,1] 
2

numpy_indexed пакет (отказ от ответственности: Я ее автор) обеспечивает элегантное и эффективное векторизованную решение такого рода проблемы :

import numpy_indexed as npi 
remapped_array = npi.remap(theArray, list(dict.keys()), list(dict.values())) 

метод реализован похож на searchsorted подхода, основанного на упомянутом Жан Lescut, но даже более общий характер. Например, элементы массива не должны быть ints, но могут быть любыми типами, даже nd-subarrays сами; но он должен достичь такого же качества.

+0

Можете ли вы изменить этот однострочный слой, чтобы добиться замены элементов исходного списка, которые не составляют словарные ключи с чем-то другим, например, константа? (вместо того, чтобы оставить исходное значение) – Tony

+0

Не с текущим вложением пакета, но функция переназначения имеет «недостающий» kwarg, и если вы откроете источник функции, вы увидите, что добавление такого поведения будет действительно быть легким. Я делаю это для предстоящего выпуска; до тех пор не стесняйтесь копировать-вставлять источник, если хотите. –

0

Полностью Векторизованное решение с использованием np.in1d и np.searchsorted:

replace = numpy.array([list(replace.keys()), list(replace.values())]) # Create 2D replacement matrix 
mask = numpy.in1d(data, replace[0, :])         # Find elements that need replacement 
data[mask] = replace[1, numpy.searchsorted(replace[0, :], data[mask])] # Replace elements 
Смежные вопросы