【问题标题】:Shuffling non-zero elements of each row in an array - Python / NumPy改组数组中每一行的非零元素 - Python / NumPy
【发布时间】:2018-01-27 15:07:58
【问题描述】:

我有一个相对稀疏的数组,我想遍历每一行并只打乱非零元素。

示例输入:

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

【问题讨论】:

    标签: python numpy vectorization shuffle


    【解决方案1】:

    您可以使用具有显式非零索引的非就地 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]])
    

    【讨论】:

      【解决方案2】:

      我想我找到了三线?

      i, j = np.nonzero(a.astype(bool))
      k = np.argsort(i + np.random.rand(i.size))
      a[i,j] = a[i,j[k]]
      

      【讨论】:

      • 是的,非常接近!实际上可能比我想象的要好。
      • rand 肯定比permute 快,但由于某种原因我不能完全掌握nonzero 似乎比where 稍慢。
      • 显然np.nonzero(a!=0) 似乎比np.nonzero(a) 快一点。所以,我在基准测试帖子中使用了它。
      • 谢谢,假期跑了,所以不能再调整它了!
      • a.astype(bool) 的平均速度似乎快了一点(尽管改进似乎小于 %timeit 从运行到运行的差异)
      【解决方案3】:

      正如承诺的那样,这是赏金期的第四天,这是我对矢量化解决方案的尝试。所涉及的步骤在下面进行了一些详细说明:

      • 为了便于参考,我们将输入数组称为a。每行生成覆盖行长度范围的唯一索引。为此,我们可以简单地生成与输入数组形状相同的随机数,并沿每一行获取argsort 索引,这将是那些唯一索引。这个想法之前在this post中已经探索过了。

      • 索引到输入数组的每一行,这些索引作为列索引。因此,我们在这里需要advanced-indexing。现在,这给了我们一个数组,其中每一行都被打乱了。我们称之为b

      • 1234563 .这是因为 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]])
      

      【讨论】:

        【解决方案4】:

        矢量化解决方案的基准测试

        我们希望在这篇文章中对矢量化解决方案进行基准测试。现在,向量化试图避免循环遍历每一行并进行洗牌。因此,输入数组的设置涉及更多的行数。

        方法-

        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 万行数据集的案例。

        案例 #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 控制台时运行)-

        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 索引。因此,对于给定数量的输入非零,它的时间比前两种方法更陡峭。

        结论:

        在 60% 非零标记之前,方法 #1 似乎表现不错。对于更多非零并且如果行长度很小,方法#3 似乎表现不错。

        【讨论】:

        • 嗯。在我的机器上,对于较小的数据集,我的算法比你的算法快 25%。我在205ms 进来,而你的大约是255ms。不过你的规模更大。
        • @DanielF 你能通过 pastebin 分享你对这四种方法的两个数据集的时间安排吗?
        • 有点希望 numpy.argsort 有一个原生的插入排序实现——这可能会让我在更大的数据集上越界,因为我的索引数组开始几乎是排序的。
        • 很遗憾没有下班。我可以在家里的电脑上尝试,但结果可能会有所不同。
        • @DanielF 或者只是将计时结果复制并粘贴到此处作为 cmets?
        【解决方案5】:

        我想出了这个:

        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 循环,并且在多行数组上会更快。

        【讨论】:

        • 最新的编辑似乎有效!在社区 wiki 帖子中添加了时间。我不得不在这一行进行编辑:labels, idx = nz = a.nonzero()。让我知道这是否是一个错误的编辑。
        • @Divakar:谢谢! *idx 使其与 n-D 数组兼容,但它与所有 python 版本不兼容,问题中未提及此功能。所以你的编辑对我来说没问题。
        【解决方案6】:

        这是矢量化解决方案的一种可能性:

        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 只提供平面索引,这会在行之间打乱数组元素。使用添加的偏移量对排列后的值进行排序可确保不会发生跨行混洗。

        【讨论】:

        • 一点也不差!
        【解决方案7】:

        这是你的两个班轮,无需安装 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
        

        【讨论】:

        • 仅仅因为它可以是单行的,并不意味着它应该是。
        猜你喜欢
        • 2011-04-17
        • 2018-12-03
        • 2019-02-17
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多