Заменить элементы в массиве numpy, избегая циклов - PullRequest
0 голосов
/ 05 ноября 2018

У меня довольно большой 1-мерный массив Xold с заданными значениями. Эти значения должны быть заменяется в соответствии с правилом, заданным двумерным массивом Y: Примером будет

Xold=np.array([0,1,2,3,4])
Y=np.array([[0,0],[1,100],[3,300],[4,400],[2,200]])

Всякий раз, когда значение в Xold совпадает со значением в Y [:, 0], новое значение в Xnew должно быть соответствующим значением в Y [:, 1]. Это достигается двумя вложенными циклами for:

Xnew=np.zeros(len(Xold))
for i in range(len(Xold)):
for j in range(len(Y)):
    if Xold[i]==Y[j,0]:
        Xnew[i]=Y[j,1]

В данном примере это дает Xnew=[0,100,200,300,400]. Однако для больших наборов данных эта процедура довольно медленная. Какой более быстрый и элегантный способ выполнить эту задачу?

Ответы [ 8 ]

0 голосов
/ 06 ноября 2018

ВЫБОР БЫСТРОГО МЕТОДА

Ответы на этот вопрос предоставили хороший набор способов замены элементов в массиве numpy. Давайте проверим, какой из них будет самым быстрым.

TL; DR: Победителем индексации является Numpy

 def meth1(): # suggested by @Slam
    for old, new in Y:  
        Xold[Xold == old] = new

 def meth2(): # suggested by myself, convert y_dict = dict(Y) first
     [y_dict[i] if i in y_dict.keys() else i for i in Xold]

 def meth3(): # suggested by @Eelco Hoogendoom, import numpy_index as npi first
     npi.remap(Xold, keys=Y[:, 0], values=Y[:, 1])

 def meth4(): # suggested by @Brad Solomon, import pandas as pd first 
     pd.Series(Xold).map(pd.Series(Y[:, 1], index=Y[:, 0])).values

  # suggested by @jdehesa. create Xnew = Xold.copy() and index
  # idx = np.searchsorted(Xold, Y[:, 0]) first
  def meth5():             
     Xnew[idx] = Y[:, 1]

Не очень удивительные результаты

 In [39]: timeit.timeit(meth1, number=1000000)                                                                      
 Out[39]: 12.08

 In [40]: timeit.timeit(meth2, number=1000000)                                                                      
 Out[40]: 2.87

 In [38]: timeit.timeit(meth3, number=1000000)                                                                      
 Out[38]: 55.39

 In [12]: timeit.timeit(meth4, number=1000000)                                                                                      
 Out[12]: 256.84

 In [50]: timeit.timeit(meth5, number=1000000)                                                                                      
 Out[50]: 1.12

Итак, старое доброе понимание списка является вторым по быстродействию, и выигрышный подход - это индексирование в сочетании с searchsorted().

0 голосов
/ 05 ноября 2018

Пакет numpy_indexed (отказ от ответственности; я его автор) содержит эффективную векторизованную функцию, которая решает общую проблему:

import numpy_indexed as npi
Xnew = npi.remap(Xold, keys=Y[:, 0], values=Y[:, 1])

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

Не уверен, как это сравнивается с пандами в плане производительности; но один из вариантов дизайна в этой библиотеке заключается в том, что выполнение элементарных операций, подобных этой (или группирование и т. д.), не должно включать создание целого нового типа данных, такого как Серия или Таблица, что всегда беспокоило меня по поводу использования панд для этого типа. вещи.

0 голосов
/ 05 ноября 2018

Вы можете преобразовать Y в словарь с помощью y = dict(Y) и затем запустить следующее понимание списка

[y[i] if i in y.keys() else i for i in Xold]
0 голосов
/ 05 ноября 2018

Решение с pd.Series.map()

Если вы открыты для использования библиотеки Pandas, вы также можете сделать это векторизованным способом с .map():

>>> import pandas as pd
>>> pd.Series(Xold).map(pd.Series(Y[:, 1], index=Y[:, 0]))                                                                                                                                                                    
0      0
1    100
2    200
3    300
4    400
dtype: int64

>>> pd.Series(Xold).map(pd.Series(Y[:, 1], index=Y[:, 0])).values                                                                                                                                                            
array([  0, 100, 200, 300, 400])

Для подписи a.map(b), a ищет соответствующие ей записи в индексе b и сопоставляется с соответствующими значениями в b.

b здесь pd.Series(Y[:, 1], index=Y[:, 0]), который использует 0-й столбец в качестве индекса и 1-й столбец в качестве значений, с которыми сопоставляются.


Использование pandas.core.algorithms напрямую

Под капотом , здесь будет использоваться .get_indexer() и реализованный на Cython take_1d():

indexer = mapper.index.get_indexer(values)
new_values = algorithms.take_1d(mapper._values, indexer)

Зная, что если массивы действительно массивные, вы можете сократить некоторые накладные расходы, как это:

from pandas.core import algorithms

indexer = pd.Index(Y[:, 0]).get_indexer(Xold)  
mapped = algorithms.take_1d(Y[:, 1], indexer)
0 голосов
/ 05 ноября 2018

Вы можете использовать slicing функции в сочетании с методом argsort.

Xnew = Y[Y[:,1].argsort()][:, 1][Xold] 

выход

array([  0, 100, 200, 300, 400])
0 голосов
/ 05 ноября 2018

Мы можем использовать np.searchsorted для общего случая, когда данные в первом столбце Y не обязательно отсортированы -

sidx = Y[:,0].argsort()
out = Y[sidx[np.searchsorted(Y[:,0], Xold, sorter=sidx)],1]

Пробный прогон -

In [53]: Xold
Out[53]: array([14, 10, 12, 13, 11])

In [54]: Y
Out[54]: 
array([[ 10,   0],
       [ 11, 100],
       [ 13, 300],
       [ 14, 400],
       [ 12, 200]])

In [55]: sidx = Y[:,0].argsort()
    ...: out = Y[sidx[np.searchsorted(Y[:,0], Xold, sorter=sidx)],1]

In [56]: out
Out[56]: array([400,   0, 200, 300, 100])

Если не у всех элементов есть соответствующие сопоставления, то нам нужно проделать еще немного работы, например, так:

sidx = Y[:,0].argsort()
sorted_indx = np.searchsorted(Y[:,0], Xold, sorter=sidx)
sorted_indx[sorted_indx==len(sidx)] = len(sidx)-1
idx_out = sidx[sorted_indx]
out = Y[idx_out,1]
out[Y[idx_out,0]!=Xold] = 0 # NA values as 0s
0 голосов
/ 05 ноября 2018

Вот одна из возможностей:

import numpy as np

Xold = np.array([0, 1, 2, 3, 4])
Y = np.array([[0, 0], [1, 100], [3, 300], [4, 400], [2, 200]])
# Check every X value against every Y first value
m = Xold == Y[:, 0, np.newaxis]
# Check which elements in X are among Y first values
# (so values that are not in Y are not replaced)
m_X = np.any(m, axis=0)
# Compute replacement
# Xold * (1 - m_X) are the non-replaced values
# np.sum(Y[:, 1, np.newaxis] * m, axis=0) * m_X are the replaced values
Xnew = Xold * (1 - m_X) + np.sum(Y[:, 1, np.newaxis] * m, axis=0) * m_X
print(Xnew)

Выход:

[  0 100 200 300 400]

Этот метод работает более или менее в каждом случае (несортированные массивы, многократные повторения значений в X, значения в X не заменяются, значения в Y не заменяют ничего в X), кроме случаев, когда вы даете две замены для одного и того же значения в Да, это было бы неправильно в любом случае. Однако его сложность во времени и пространстве является результатом размеров X и Y. Если у вашей задачи есть дополнительные ограничения (данные сортируются, нет повторений и т. Д.), Возможно, можно сделать что-то лучше. Например, если X отсортирован без повторяющихся элементов и каждое значение в Y заменяет значение в X (как в вашем примере), это, вероятно, будет быстрее:

import numpy as np

Xold = np.array([0, 1, 2, 3, 4])
Y = np.array([[0, 0], [1, 100], [3, 300], [4, 400], [2, 200]])
idx = np.searchsorted(Xold, Y[:, 0])
Xnew = Xold.copy()
Xnew[idx] = Y[:, 1]
print(Xnew)
# [  0 100 200 300 400]
0 голосов
/ 05 ноября 2018

Первое улучшение, которое вы можете сделать, это использовать индексирование NumPy, но у вас все еще будет 1 цикл:

for old, new in Y: 
    Xold[Xold == old] = new
...