8

I have a numpy boolean array

w=np.array([True,False,True,True,False,False,False])

I would like to get the index of the first time there are at n_at_least false values. For instance here

`n_at_least`=1 -> desired_index=1

`n_at_least`=3 -> desired_index=4

I have tried

np.cumsum(~w)

which does increase every time a False value is encountered. However, when True is encountered the counter is not starting from 0 again so I only get the total count of False elements rather than the count of the last consecutive ones.

00__00__00
  • 4,834
  • 9
  • 41
  • 89

6 Answers6

7

Here's a vectorized solution that finds the start, stop indices and hence lengths of islands of zeros and finally uses argmax to get the starting index of the first island satisfying the criteria of zeros count being >= n -

def first_occ_index(w, n):
    idx = np.flatnonzero(np.r_[True, w, True])
    lens = np.diff(idx) - 1
    return idx[(lens >= n).argmax()]

Sample run -

In [107]: w
Out[107]: array([ True, False,  True,  True, False, False, False])

In [108]: first_occ_index(w, n=1)
Out[108]: 1

In [109]: first_occ_index(w, n=3)
Out[109]: 4
FHTMitchell
  • 11,793
  • 2
  • 35
  • 47
Divakar
  • 218,885
  • 19
  • 262
  • 358
4

I think you're falling into the numpy trap of only wanting to use numpy functions. What's wrong with python? This solution is O(n)

def f(array, n_at_least):
    curr_found_false = 0
    curr_index = 0
    for index, elem in enumerate(array):
        if not elem:
            if curr_found_false == 0:
                curr_index = index
            curr_found_false += 1
            if curr_found_false == n_at_least:
                return curr_index
        else:
            curr_found_false = 0

Outputs

w=np.array([True,False,True,True,False,False,False])
f(w, 1)
# --> 1
f(w, 3)
# --> 4
FHTMitchell
  • 11,793
  • 2
  • 35
  • 47
  • Good answer. But I would claim that most people are falling in to the `list` "trap" :). The fact `numpy` is efficient is *because* it's 3rd party. – jpp Apr 06 '18 at 14:41
  • @jpp I suppose that's true. But looking for a numpy solution isn't always worth your time for the benefits it brings. That being said I would use one if one is available. – FHTMitchell Apr 06 '18 at 14:54
  • Just FYI, it can still be done faster using numpy and convolution. See https://stackoverflow.com/a/62747440/13636407 – paime Jul 07 '20 at 21:29
4

Here is an O(n) numpy solution:

>>> def first_consec(A, n):
...     A = np.r_[True, A, True]
...     switch, = np.where(A[:-1]!=A[1:])
...     runs = switch[1::2] - switch[::2]
...     idx = np.argmax(runs >= n)
...     if runs[idx] < n:
...         return None
...     return switch[2*idx]
... 
>>> first_consec(w, 4)
>>> first_consec(w, 3)
4
>>> first_consec(w, 2)
4
>>> first_consec(w, 1)
1
Paul Panzer
  • 51,835
  • 3
  • 54
  • 99
2

I think for this linear search operation a python implementation is ok. My suggestion looks like this:

def find_block(arr, n_at_least=1):
    current_index = 0
    current_count = 0
    for index, item in enumerate(arr):
         if item:
             current_count = 0
             current_index = index + 1
         else:
             current_count += 1
         if current_count == n_at_least:
             return current_index
    return None # Make sure this is outside for loop

Running this function yields the following outputs:

>>> import numpy
>>> w = numpy.array([True, False, True, True, False, False, False])
>>> find_block(w, n_at_least=1)
1
>>> find_block(w, n_at_least=3)
4
>>> find_block(w, n_at_least=4)
>>> # None
Lukisn
  • 190
  • 8
1

It should work this way:

def n_at_least(n):
    for i in range(len(w)):
         if not any(w[i:i+n]):
             return i

However I don't know if there is a better way...

wohe1
  • 755
  • 7
  • 26
1

This is one way using a generator expression with slicing:

w = np.array([True,False,True,True,False,False,False])

n = 2
val = False

res = next((i for i, j in enumerate(w[k:k+n] for k in range(len(w)-n+1)) \
            if np.all(j==val)), None)

# 4
jpp
  • 159,742
  • 34
  • 281
  • 339