使用多个自定义索引范围构建numpy数组,而无需显式循环

时间:2016-06-03 23:23:40

标签: python arrays performance numpy vectorization

在Numpy中,是否有一种pythonic方法来创建array3,其中自定义范围来自array1和array2而没有循环?迭代范围的直接解决方案有效,但由于我的数组遇到了数百万个项目,我正在寻找更有效的解决方案(也可能是语法糖)。

例如,

File

结果:array1 = np.array([10, 65, 200]) array2 = np.array([14, 70, 204]) array3 = np.concatenate([np.arange(array1[i], array2[i]) for i in np.arange(0,len(array1))]) print array3

4 个答案:

答案 0 :(得分:5)

假设范围不重叠,您可以构建一个非零的掩码,其中索引介于array1array2指定的范围之间,然后使用np.flatnonzero获取数组索引 - 期望的array3

import numpy as np

array1 = np.array([10, 65, 200]) 
array2 = np.array([14, 70, 204])

first, last = array1.min(), array2.max()
array3 = np.zeros(last-first+1, dtype='i1')
array3[array1-first] = 1
array3[array2-first] = -1
array3 = np.flatnonzero(array3.cumsum())+first
print(array3)

产量

[ 10  11  12  13  65  66  67  68  69 200 201 202 203]

对于大型len(array1)using_flatnonzero可能明显快于using_loop

def using_flatnonzero(array1, array2):
    first, last = array1.min(), array2.max()
    array3 = np.zeros(last-first+1, dtype='i1')
    array3[array1-first] = 1
    array3[array2-first] = -1
    return np.flatnonzero(array3.cumsum())+first

def using_loop(array1, array2):
    return np.concatenate([np.arange(array1[i], array2[i]) for i in
                           np.arange(0,len(array1))])


array1, array2 = (np.random.choice(range(1, 11), size=10**4, replace=True)
                  .cumsum().reshape(2, -1, order='F'))


assert np.allclose(using_flatnonzero(array1, array2), using_loop(array1, array2))
In [260]: %timeit using_loop(array1, array2)
100 loops, best of 3: 9.36 ms per loop

In [261]: %timeit using_flatnonzero(array1, array2)
1000 loops, best of 3: 564 µs per loop

如果范围重叠,则using_loop将返回包含重复项的array3using_flatnonzero返回一个没有重复项的数组。

解释:让我们看一下

的小例子
array1 = np.array([10, 65, 200]) 
array2 = np.array([14, 70, 204])

目标是在下面构建一个类似goal的数组。 1位于索引值[ 10, 11, 12, 13, 65, 66, 67, 68, 69, 200, 201, 202, 203](即array3):

In [306]: goal
Out[306]: 
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1,
       1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1], dtype=int8)

获得goal数组后,可以通过调用array3获取np.flatnonzero

In [307]: np.flatnonzero(goal)
Out[307]: array([ 10,  11,  12,  13,  65,  66,  67,  68,  69, 200, 201, 202, 203])

goal的长度与array2.max()相同:

In [308]: array2.max()
Out[308]: 204

In [309]: goal.shape
Out[309]: (204,)

所以我们可以从分配

开始
goal = np.zeros(array2.max()+1, dtype='i1')

然后在由array1给出的索引处array2和-1&#39处给出的索引位置填写1:

In [311]: goal[array1] = 1
In [312]: goal[array2] = -1
In [313]: goal
Out[313]: 
array([ 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  0,  0,  0, -1,  0,  0,
        0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
        0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
        0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  0,  0,
        0,  0, -1,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
        0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
        0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
        0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
        0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
        0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
        0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
        0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  0,  0,  0,
       -1], dtype=int8)

现在应用cumsum(累积和)会生成所需的goal数组:

In [314]: goal = goal.cumsum(); goal
Out[314]: 
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1,
       1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0])

In [315]: np.flatnonzero(goal)
Out[315]: array([ 10,  11,  12,  13,  65,  66,  67,  68,  69, 200, 201, 202, 203])

这是using_flatnonzero背后的主要理念。减去first只是为了节省一点内存。

答案 1 :(得分:2)

前瞻性方法

我将回顾如何解决这个问题。

采取问题中列出的样本。我们有 -

array1 = np.array([10, 65, 200])
array2 = np.array([14, 70, 204])

现在,看看想要的结果 -

result: [10,11,12,13,65,66,67,68,69,200,201,202,203]

让我们计算群组长度,因为我们需要那些解释下一步的解决方案。

In [58]: lens = array2 - array1

In [59]: lens
Out[59]: array([4, 5, 4])

这个想法是使用1的初始化数组,当在整个长度上累积总和时,会给出我们想要的结果。 这个累积总和将是我们解决方案的最后一步。 为什么1已初始化?好吧,因为除了我们有班次的特定地方,我们有一个数组逐步增加1的数组 对应新的团体。

现在,因为cumsum将是最后一步,所以它前面的步骤应该给我们一些像 -

array([ 10,   1,   1,   1,  52,   1,   1,   1,   1, 131,   1,   1,   1])

如前所述,1在特定地点填充了[10,52,131]10似乎来自array1中的第一个元素,但其余的是什么? 第二个5265-13(查看result)进入,其中13来自以10开头并因为的长度 第一组4。因此,如果我们执行65 - 10 - 4,我们将获得51,然后添加1以适应边界停止,我们将52,这是 期望的转移价值。同样,我们会得到131

因此,可以计算那些shifting-values,如此 -

In [62]: np.diff(array1) - lens[:-1]+1
Out[62]: array([ 52, 131])

接下来,为了让那些发生这种转变的shifting-places,我们可以简单地对组长度进行累积求和 -

In [65]: lens[:-1].cumsum()
Out[65]: array([4, 9])

为了完整起见,我们需要为0预先附加shifting-places数组array1[0]shifting-values

因此,我们将逐步展示我们的方法!

放回件

1]获取每组的长度:

lens = array2 - array1

2]获取发生转变的指数和要放入1初始化数组的值:

shift_idx = np.hstack((0,lens[:-1].cumsum()))
shift_vals = np.hstack((array1[0],np.diff(array1) - lens[:-1]+1))

3]设置1的初始化ID数组,用于在以下步骤中列出的那些索引中插入这些值:

id_arr = np.ones(lens.sum(),dtype=array1.dtype)
id_arr[shift_idx] = shift_vals

4]最后对ID数组进行累加求和:

output = id_arr.cumsum() 

以函数格式列出,我们有 -

def using_ones_cumsum(array1, array2):
    lens = array2 - array1
    shift_idx = np.hstack((0,lens[:-1].cumsum()))
    shift_vals = np.hstack((array1[0],np.diff(array1) - lens[:-1]+1))
    id_arr = np.ones(lens.sum(),dtype=array1.dtype)
    id_arr[shift_idx] = shift_vals
    return id_arr.cumsum()

它也适用于重叠范围!

In [67]: array1 = np.array([10, 11, 200]) 
    ...: array2 = np.array([14, 18, 204])
    ...: 

In [68]: using_ones_cumsum(array1, array2)
Out[68]: 
array([ 10,  11,  12,  13,  11,  12,  13,  14,  15,  16,  17, 200, 201,
       202, 203])

运行时测试

让我们对@unutbu's flatnonzero based solution中针对其他矢量化方法的方法进行计时,已经证明这种方法比循环方法要好得多 -

In [38]: array1, array2 = (np.random.choice(range(1, 11), size=10**4, replace=True)
    ...:                   .cumsum().reshape(2, -1, order='F'))

In [39]: %timeit using_flatnonzero(array1, array2)
1000 loops, best of 3: 889 µs per loop

In [40]: %timeit using_ones_cumsum(array1, array2)
1000 loops, best of 3: 235 µs per loop

改进!

现在,代码NumPy并不喜欢追加。因此,对于稍微改进的版本,可以避免这些np.hstack次调用 -

def get_ranges_arr(starts,ends):
    counts = ends - starts
    counts_csum = counts.cumsum()
    id_arr = np.ones(counts_csum[-1],dtype=int)
    id_arr[0] = starts[0]
    id_arr[counts_csum[:-1]] = starts[1:] - ends[:-1] + 1
    return id_arr.cumsum()

让我们反对我们原来的做法 -

In [151]: array1,array2 = (np.random.choice(range(1, 11),size=10**4, replace=True)\
     ...:                                      .cumsum().reshape(2, -1, order='F'))

In [152]: %timeit using_ones_cumsum(array1, array2)
1000 loops, best of 3: 276 µs per loop

In [153]: %timeit get_ranges_arr(array1, array2)
10000 loops, best of 3: 193 µs per loop

因此,我们在那里有 30% 性能提升!

答案 2 :(得分:0)

这是我结合vectorizeconcatenate的方法:

<强>实施

import numpy as np

array1, array2 = np.array([10, 65, 200]), np.array([14, 70, 204])

ranges = np.vectorize(lambda a, b: np.arange(a, b), otypes=[np.ndarray])
result = np.concatenate(ranges(array1, array2), axis=0)

print result
# [ 10  11  12  13  65  66  67  68  69 200 201 202 203]

<强>性能

%timeit np.concatenate(ranges(array1, array2), axis=0)
  

100000个循环,最佳3:每循环13.9μs

答案 3 :(得分:0)

你是说这个吗?

In [440]: np.r_[10:14,65:70,200:204]
Out[440]: array([ 10,  11,  12,  13,  65,  66,  67,  68,  69, 200, 201, 202, 203])

或概括:

In [454]: np.r_[tuple([slice(i,j) for i,j in zip(array1,array2)])]
Out[454]: array([ 10,  11,  12,  13,  65,  66,  67,  68,  69, 200, 201, 202, 203])

虽然这确实涉及双循环,但是生成切片的显式循环和r_内的切换将切片转换为arange

    for k in range(len(key)):
        scalar = False
        if isinstance(key[k], slice):
            step = key[k].step
            start = key[k].start
                ...
                newobj = _nx.arange(start, stop, step)

我之所以提到这一点,是因为它表明numpy开发人员认为你的迭代是正常的。

我希望@ unutbu的切肉刀,如果有点迟钝(我还没弄清楚它到底在做什么),解决方案是你最好的速度机会。 cumsum是一个很好的工具,当您需要使用范围而不是长度不同时。当使用许多小范围时,它可能获得最多。我不认为它适用于重叠范围。

=====

np.vectorize使用np.frompyfunc。所以这个迭代也可以表示为:

In [467]: f=np.frompyfunc(lambda x,y: np.arange(x,y), 2,1)

In [468]: f(array1,array2)
Out[468]: 
array([array([10, 11, 12, 13]), array([65, 66, 67, 68, 69]),
       array([200, 201, 202, 203])], dtype=object)

In [469]: timeit np.concatenate(f(array1,array2))
100000 loops, best of 3: 17 µs per loop

In [470]: timeit np.r_[tuple([slice(i,j) for i,j in zip(array1,array2)])]
10000 loops, best of 3: 65.7 µs per loop

使用@ Darius的vectorize解决方案:

In [474]: timeit result = np.concatenate(ranges(array1, array2), axis=0)
10000 loops, best of 3: 52 µs per loop

vectorize必须做一些额外的工作才能更有效地使用广播。如果array1更大,相对速度可能会发生变化。

@ unutbu的解决方案对于这个小array1并不特别。

In [478]: timeit using_flatnonzero(array1,array2)
10000 loops, best of 3: 57.3 µs per loop

OP解决方案,迭代没有我的r_中间人是好的

In [483]: timeit array3 = np.concatenate([np.arange(array1[i], array2[i]) for i in np.arange(0,len(array1))])
10000 loops, best of 3: 24.8 µs per loop

通常情况下,使用少量循环,列表理解比较高级numpy操作更快。

对于@ unutbu更大的测试用例,我的时间与他的时间一致 - 速度提高了17倍。

===================

对于小样本阵列,@ Divakar的解决方案速度较慢,但​​对于大型解决方案,速度比@ unutbu快3倍。因此它有更多的设置成本,但缩放速度较慢。