web-dev-qa-db-ja.com

配列内の各行のゼロ以外の要素をシャッフルする-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])
_
35
Bar

非インプレースを使用できます 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]])
15
randomir

スリーライナーを見つけたと思いますか?

i, j = np.nonzero(a.astype(bool))
k = np.argsort(i + np.random.Rand(i.size))
a[i,j] = a[i,j[k]]
8
Daniel F

約束通り、これはバウンティ期間の4日目です。これが、ベクトル化されたソリューションでの私の試みです。関連する手順については、以下で詳しく説明します。

  • 簡単に参照できるように、入力配列をaと呼びましょう。行の長さの範囲をカバーする、行ごとの一意のインデックスを生成します。このために、入力配列と同じ形状の乱数を単純に生成し、各行に沿ってargsortインデックスを取得できます。これは、これらの一意のインデックスになります。このアイデアは以前に this post

  • これらのインデックスを列インデックスとして、入力配列の各行にインデックスを付けます。したがって、 advanced-indexing ここ。これで、各行がシャッフルされた配列が得られます。それをbと呼びましょう。

  • シャッフルは行ごとに制限されているため、単にブールインデックスを使用すると:b[b!=0]、ゼロ以外の要素がシャッフルされ、行ごとのゼロ以外の長さに制限されます。これは、NumPy配列の要素が行優先の順序で格納されるためです。したがって、ブールインデックスを使用すると、次の行に移動する前に、最初に各行でシャッフルされた非ゼロ要素が選択されます。繰り返しますが、aに対して同様にブールインデックスを使用する場合、つまりa[a!=0]、次の行に移動する前に、最初に各行のゼロ以外の要素を同様に取得し、これらは元の順序になります。したがって、最後のステップは、マスクされた要素を取得することですb[b!=0]そしてマスクされた場所に割り当てますa[a!=0]

したがって、上記の3つのステップをカバーする実装は次のようになります。

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]])
8
Divakar

ベクトル化されたソリューションのベンチマーク

この投稿では、ベクトル化されたソリューションのベンチマークを検討しています。ここで、ベクトル化は、各行をループしてシャッフルするループを回避しようとします。したがって、入力配列のセットアップには、より多くの行が含まれます。

アプローチ-

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のサンプルを使用し、それらをより多くの行としてスタックして、より大きなデータセットを取得することは合理的であるように思われました。したがって、その設定では、200万行と2000万行のデータセットを持つ2つのケースを作成できます。

ケース#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

ケース#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マジック関数を使用します。したがって、IPythonコンソールにコピーして貼り付けるoponを実行する必要があります)-

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インデックスを取得します。したがって、入力内のゼロ以外の特定の数の場合、そのタイミングは最初の2つのアプローチよりも急勾配になります。

結論:

アプローチ#1は、60%がゼロ以外のマークになるまでかなりうまくいっているようです。より多くの非ゼロの場合、および行の長さが小さい場合、アプローチ#3は適切に実行されているようです。

7
Divakar

私はそれを思いついた:

_nz = a.nonzero()                      # Get nonzero indexes
a[nz] = np.random.permutation(a[nz])  # Shuffle nonzero values with mask
_

他の提案されたソリューションよりも単純に見える(そして少し速くなる?).


編集:行を混在させない新しいバージョン

_ 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ループが置き換えられ、行数の多い配列で高速になります。

4
FabienP

これが、numpyをインストールする必要のない2つのライナーです。

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)])

ただし、1つのライナーが気に入らない場合は、これをジェネレーターにすることができます。

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)

または、出力としてリストが必要で、1行の内包表記が気に入らない場合

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
3
codykochmann

これは、ベクトル化されたソリューションの1つの可能性です。

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はフラットなインデックスのみを提供し、行全体で配列要素をシャッフルします。オフセットを追加して並べ替えられた値を並べ替えると、行間でシャッフルが発生しないようになります。

3
kazemakase