Перетасовка ненулевых элементов каждой строки в массиве-Python / NumPy
у меня есть массив, который относительно разрежен, и я хотел бы пройти через каждую строку и перетасовать только ненулевые элементы.
Входные Данные:
[2,3,1,0]
[0,0,2,1]
Пример:
[2,1,3,0]
[0,0,1,2]
обратите внимание, как нули не изменились позиции.
перемешать все элементы в каждой строке (включая нули), что я могу сделать это:
for i in range(len(X)):
np.random.shuffle(X[i, :])
то, что я пытался сделать тогда, это:
for i in range(len(X)):
np.random.shuffle(X[i, np.nonzero(X[i, :])])
но это не имеет никакого эффекта. Я заметил, что тип возврата X[i, np.nonzero(X[i, :])]
отличается от X[i, :]
что может быть
причина.
In[30]: X[i, np.nonzero(X[i, :])]
Out[30]: array([[23, 5, 29, 11, 17]])
In[31]: X[i, :]
Out[31]: array([23, 5, 29, 11, 17])
7 ответов
вы можете использовать non-inplace numpy.random.permutation
с явным ненулевым индексированием:
>>> X = np.array([[2,3,1,0], [0,0,2,1]])
>>> for i in range(len(X)):
... idx = np.nonzero(X[i])
... X[i][idx] = np.random.permutation(X[i][idx])
...
>>> X
array([[3, 2, 1, 0],
[0, 0, 2, 1]])
Я думаю, что нашел три лайнера?
i, j = np.nonzero(a.astype(bool))
k = np.argsort(i + np.random.rand(i.size))
a[i,j] = a[i,j[k]]
как и было обещано, это четвертый день периода щедрости, вот моя попытка векторизованного решения. Этапов описаны в некоторых деталях ниже :
для удобства, давайте назовем входной массив как
a
. Создайте уникальные индексы для каждой строки, которые охватывают диапазон длины строки. Для этого мы можем просто генерировать случайные числа той же формы, что и входной массив, и получитьargsort
индексы вдоль каждой строки, которые были бы уникальными индексы. Эта идея была исследована ранее вthis post
.Index в каждую строку входного массива с этими индексами в качестве индексов столбцов. Таким образом, нам понадобится
advanced-indexing
здесь. Теперь, это дает нам массив с каждой строки перемешиваются. Назовем этоb
.поскольку перетасовка ограничена каждой строкой, если мы просто используем логическое индексирование:
b[b!=0]
, мы получили бы ненулевые элементы перемешивается, а также ограничивается длинами ненулей в строке. Это связано с тем, что элементы в массиве NumPy хранятся в порядке строк, поэтому при булевом индексировании он сначала выбрал бы перетасованные ненулевые элементы в каждой строке, прежде чем перейти к следующей строке. Опять же, если мы используем логическое индексирование аналогично дляa
, то естьa[a!=0]
, мы бы точно получили ненулевых элементов в каждой строке, прежде чем перейти к следующей строке, и они будут в их исходный порядок. Таким образом, последним шагом было бы просто захватить маскированные элементыb[b!=0]
и назначить в маскахa[a!=0]
.
таким образом, реализация, охватывающая вышеупомянутые три шага, будет -
m,n = a.shape
rand_idx = np.random.rand(m,n).argsort(axis=1) #step1
b = a[np.arange(m)[:,None], rand_idx] #step2
a[a!=0] = b[b!=0] #step3
пример пошагового выполнения может сделать вещи более ясными -
In [50]: a # Input array
Out[50]:
array([[ 8, 5, 0, -4],
[ 0, 6, 0, 3],
[ 8, 5, 0, -4]])
In [51]: m,n = a.shape # Store shape information
# Unique indices per row that covers the range for row length
In [52]: rand_idx = np.random.rand(m,n).argsort(axis=1)
In [53]: rand_idx
Out[53]:
array([[0, 2, 3, 1],
[1, 0, 3, 2],
[2, 3, 0, 1]])
# Get corresponding indexed array
In [54]: b = a[np.arange(m)[:,None], rand_idx]
# Do a check on the shuffling being restricted to per row
In [55]: a[a!=0]
Out[55]: array([ 8, 5, -4, 6, 3, 8, 5, -4])
In [56]: b[b!=0]
Out[56]: array([ 8, -4, 5, 6, 3, -4, 8, 5])
# Finally do the assignment based on masking on a and b
In [57]: a[a!=0] = b[b!=0]
In [58]: a # Final verification on desired result
Out[58]:
array([[ 8, -4, 0, 5],
[ 0, 6, 0, 3],
[-4, 8, 0, 5]])
бенчмаркинг для векторизованных решений
мы ищем эталонные векторизованные решения в этом посте. Теперь векторизация пытается избежать цикла, который мы будем петлять через каждую строку и делать перетасовку. Таким образом, настройка входного массива включает большее количество строк.
подходов -
def app1(a): # @Daniel F's soln
i, j = np.nonzero(a.astype(bool))
k = np.argsort(i + np.random.rand(i.size))
a[i,j] = a[i,j[k]]
return a
def app2(x): # @kazemakase's soln
r, c = np.where(x != 0)
n = c.size
perm = np.random.permutation(n)
i = np.argsort(perm + r * n)
x[r, c] = x[r, c[i]]
return x
def app3(a): # @Divakar's soln
m,n = a.shape
rand_idx = np.random.rand(m,n).argsort(axis=1)
b = a[np.arange(m)[:,None], rand_idx]
a[a!=0] = b[b!=0]
return a
from scipy.ndimage.measurements import labeled_comprehension
def app4(a): # @FabienP's soln
def func(array, idx):
r[idx] = np.random.permutation(array)
return True
labels, idx = nz = a.nonzero()
r = a[nz]
labeled_comprehension(a[nz], labels + 1, np.unique(labels + 1),\
func, int, 0, pass_positions=True)
a[nz] = r
return a
процедура бенчмаркинга #1
для справедливого бенчмаркинга казалось разумным использовать Образец OP и просто стек их как больше строк, чтобы получить больший набор данных. Таким образом, с этой настройкой мы могли бы создать два случая с 2 миллионами и 20 миллионами наборов данных строк.
Case #1: большой набор данных с 2*1000,000
строки
In [174]: a = np.array([[2,3,1,0],[0,0,2,1]])
In [175]: a = np.vstack([a]*1000000)
In [176]: %timeit app1(a)
...: %timeit app2(a)
...: %timeit app3(a)
...: %timeit app4(a)
...:
1 loop, best of 3: 264 ms per loop
1 loop, best of 3: 422 ms per loop
1 loop, best of 3: 254 ms per loop
1 loop, best of 3: 14.3 s per loop
Case #2: больший набор данных с 2*10,000,000
строки
In [177]: a = np.array([[2,3,1,0],[0,0,2,1]])
In [178]: a = np.vstack([a]*10000000)
# app4 skipped here as it was slower on the previous smaller dataset
In [179]: %timeit app1(a)
...: %timeit app2(a)
...: %timeit app3(a)
...:
1 loop, best of 3: 2.86 s per loop
1 loop, best of 3: 4.62 s per loop
1 loop, best of 3: 2.55 s per loop
процедура бенчмаркинга #2: обширная
для того чтобы покрыть все случаи меняя процента не-нолей в массиве входного сигнала, мы покрываем некоторые обширные сценарии бенчмаркинга. Кроме того, так как app4
казалось, намного медленнее, чем другие, для более близкого осмотра мы пропускаем этот в этом разделе.
вспомогательная функция для установки входного массива:
def in_data(n_col, nnz_ratio):
# max no. of elems that my system can handle, i.e. stretching it to limits.
# The idea is to use this to decide the number of rows and always use
# max. possible dataset size
num_elem = 10000000
n_row = num_elem//n_col
a = np.zeros((n_row, n_col),dtype=int)
L = int(round(a.size*nnz_ratio))
a.ravel()[np.random.choice(a.size, L, replace=0)] = np.random.randint(1,6,L)
return a
основной сценарий синхронизации и построения (использует магические функции IPython. Итак, необходимо запустить копирование и вставку opon на консоль IPython) -
import matplotlib.pyplot as plt
# Setup input params
nnz_ratios = np.array([0.2, 0.4, 0.6, 0.8])
n_cols = np.array([4, 5, 8, 10, 15, 20, 25, 50])
init_arr1 = np.zeros((len(nnz_ratios), len(n_cols) ))
init_arr2 = np.zeros((len(nnz_ratios), len(n_cols) ))
init_arr3 = np.zeros((len(nnz_ratios), len(n_cols) ))
timings = {app1:init_arr1, app2:init_arr2, app3:init_arr3}
for i,nnz_ratio in enumerate(nnz_ratios):
for j,n_col in enumerate(n_cols):
a = in_data(n_col, nnz_ratio=nnz_ratio)
for func in timings:
res = %timeit -oq func(a)
timings[func][i,j] = res.best
print func.__name__, i, j, res.best
fig = plt.figure(1)
colors = ['b','k','r']
for i in range(len(nnz_ratios)):
ax = plt.subplot(2,2,i+1)
for f,func in enumerate(timings):
ax.plot(n_cols,
[time for time in timings[func][i]],
label=str(func.__name__), color=colors[f])
ax.set_xlabel('No. of cols')
ax.set_ylabel('time [seconds]')
ax.grid(which='both')
ax.legend()
plt.tight_layout()
plt.title('Percentage non-zeros : '+str(int(100*nnz_ratios[i])) + '%')
plt.subplots_adjust(wspace=0.2, hspace=0.2)
выход тайминги -
замечания :
подходы #1, #2 не
argsort
на ненулевых элементах по всему входному массиву. Таким образом, он работает лучше с меньшим процентом ненулей.подход #3 создает случайные числа той же формы, что и входной массив, а затем получает
argsort
индексы для каждой строки. Таким образом, для заданного числа ненулей на входе тайминги для него более крутые, чем первые два подхода.
вывод :
подход №1, кажется, делает довольно хорошо, пока 60% ненулевой отметки. Для большего количества ненулей и если длины строк малы, подход № 3, похоже, работает прилично.
я придумал, что:
nz = a.nonzero() # Get nonzero indexes
a[nz] = np.random.permutation(a[nz]) # Shuffle nonzero values with mask
, которые выглядят проще (и немного быстрее?), чем другие предлагаемые решения.
EDIT: новая версия, которая не смешивает строки
labels, *idx = nz = a.nonzero() # get masks
a[nz] = np.concatenate([np.random.permutation(a[nz][labels == i]) # permute values
for i in np.unique(labels)]) # for each label
где первый массив a.nonzero()
(индексы ненулевых значений в axis0) используется в качестве меток. Это трюк, который не смешивает строки.
затем np.random.permutation
наносится на a[a.nonzero()]
для каждой надписи "" независимо.
якобы scipy.ndimage.measurements.labeled_comprehension
может использоваться здесь, по-видимому, не с np.random.permutation
.
и я, наконец, увидел, что это очень похоже на то, что предложил @randomir. Во всяком случае, это было только для того, чтобы заставить его работать.
EDIT2:
наконец-то он работает с scipy.ndimage.measurements.labeled_comprehension
def shuffle_rows(a):
def func(array, idx):
r[idx] = np.random.permutation(array)
return True
labels, *idx = nz = a.nonzero()
r = a[nz]
labeled_comprehension(a[nz], labels + 1, np.unique(labels + 1), func, int, 0, pass_positions=True)
a[nz] = r
return a
где:
-
func()
тасует не ноль значения -
labeled_comprehension
применяетсяfunc()
ярлык-мудрый
это заменяет предыдущий цикл for и будет быстрее на массивах со многими строками.
Это одна из возможностей для решения векторизации:
r, c = np.where(x > 0)
n = c.size
perm = np.random.permutation(n)
i = np.argsort(perm + r * n)
x[r, c] = x[r, c[i]]
задача векторизации этой проблемы заключается в том, что np.random.permutation
дает только плоские индексы, которые будут перетасовывать элементы массива по строкам. Сортировка перестановочных значений с добавленным смещением гарантирует отсутствие перетасовки по строкам.
вот ваши два лайнера без необходимости установки numpy.
from random import random
def shuffle_nonzeros(input_list):
''' returns a list with the non-zero values shuffled '''
shuffled_nonzero = iter(sorted((i for i in input_list if i!=0), key=lambda k: random()))
print([i for i in (i if i==0 else next(shuffled_nonzero) for i in input_list)])
Если вам не нравится один лайнеры, вы можете либо сделать это генератор с
def shuffle_nonzeros(input_list):
''' generator that yields a list with the non-zero values shuffled '''
random_nonzero_values = iter(sorted((i for i in input_list if i!=0), key=lambda k: random()))
for i in iterable:
if i==0:
yield i
else:
yield next(random_nonzero_values)
или если вы хотите список в качестве вывода, и не нравится одна строка понимания
def shuffle_nonzeros(input_list):
''' returns a list with the non-zero values shuffled '''
out = []
random_nonzero_values = iter(sorted((i for i in input_list if i!=0), key=lambda k: random()))
for i in iterable:
if i==0:
out.append(i)
else:
out.append(next(random_nonzero_values))
return out