Существует мнение, что использование np.take
значительно быстрее индексации массивов. Например, http://wesmckinney.com/blog/numpy-indexing-peculiarities/, Быстрое индексирование фигурных чисел и Быстрое (er) индексирование фантазийных фигур и сокращение? . Есть также предположения, что np.ix_
лучше в некоторых случаях.
Я провел некоторое профилирование, и в большинстве случаев это действительно так, хотя разница уменьшается по мере увеличения массивов.
На производительность влияет размер массива, длина индекса (для строк) и количество занятых столбцов. Количество строк, кажется, оказывает наибольшее влияние, количество столбцов в массиве также оказывает влияние, даже если индекс равен 1D. Изменение размера индекса, похоже, не сильно влияет на методы.
Итак, вопрос в два раза:
1. Почему такая большая разница в производительности между методами?
2. Когда имеет смысл использовать один метод над другим? Существуют ли какие-либо типы массивов, упорядочения или формы, для которых всегда лучше работать?
Есть много вещей, которые могут повлиять на производительность, поэтому я показал несколько из них ниже и включил код, используемый для того, чтобы попытаться сделать это воспроизводимым.
Редактировать Я обновил ось Y на графиках, чтобы показать полный диапазон значений. Это делает более ясным, что разница меньше, чем она появилась для 1D данных.
1D индекс
Анализ времени выполнения в сравнении с количеством строк показывает, что индексация довольно последовательная, с небольшим восходящим трендом. take
постоянно медленнее с увеличением числа строк.
По мере увеличения числа столбцов оба становятся медленнее, но take
имеет большее увеличение (и это все еще для одномерного индекса).
2D индекс
С 2D данными результаты аналогичны. Использование ix_
также показано, и, похоже, имеет худшую производительность в целом.
код для цифр
from pylab import *
import timeit
def get_test(M, T, C):
"""
Returns an array and random sorted index into rows
M : number of rows
T : rows to take
C : number of columns
"""
arr = randn(M, C)
idx = sort(randint(0, M, T))
return arr, idx
def draw_time(call, N=10, V='M', T=1000, M=5000, C=300, **kwargs):
"""
call : function to do indexing, accepts (arr, idx)
N : number of times to run timeit
V : string indicating to evaluate number of rows (M) or rows taken (T), or columns created(C)
** kwargs : passed to plot
"""
pts = {
'M': [10, 20, 50, 100, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000, 200000, 500000, ],
'T': [10, 50, 100, 500, 1000, 5000, 10000, 50000],
'C': [5, 10, 20, 50, 100, 200, 500, 1000],
}
res = []
kw = dict(T=T, M=M, C=C) ## Default values
for v in pts[V]:
kw[V] = v
try:
arr, idx = get_test(**kw)
except CallerError:
res.append(None)
else:
res.append(timeit.timeit(lambda :call(arr, idx), number=N))
plot(pts[V], res, marker='x', **kwargs)
xscale('log')
ylabel('runtime [s]')
if V == 'M':
xlabel('size of array [rows]')
elif V == 'T':
xlabel('number of rows taken')
elif V == 'C':
xlabel('number of columns created')
funcs1D = {
'fancy':lambda arr, idx: arr[idx],
'take':lambda arr, idx: arr.take(idx, axis=0),
}
cidx = r_[1, 3, 7, 15, 29]
funcs2D = {
'fancy2D':lambda arr, idx: arr[idx.reshape(-1, 1), cidx],
'take2D':lambda arr, idx: arr.take(idx.reshape(-1, 1)*arr.shape[1] + cidx),
'ix_':lambda arr, idx: arr[ix_(idx, cidx)],
}
def test(funcs, N=100, **kwargs):
for descr, f in funcs.items():
draw_time(f, label="{}".format(descr), N=100, **kwargs)
legend()
figure()
title('1D index, 30 columns in data')
test(funcs1D, V='M')
ylim(0, 0.25)
# savefig('perf_1D_arraysize', C=30)
figure()
title('1D index, 5000 rows in data')
test(funcs1D, V='C', M=5000)
ylim(0, 0.07)
# savefig('perf_1D_numbercolumns')
figure()
title('2D index, 300 columns in data')
test(funcs2D, V='M')
ylim(0, 0.01)
# savefig('perf_2D_arraysize')
figure()
title('2D index, 30 columns in data')
test(funcs2D, V='M')
ylim(0, 0.01)
# savefig('perf_2D_arraysize_C30', C=30)