Перетасовка ненулевых элементов каждой строки в массиве-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)

выход тайминги -

enter image description here

замечания :

  • подходы #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

где:

  1. func() тасует не ноль значения
  2. 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