Home > Mobile >  Python/Numpy/Boolean Indexing: Modify boolean array at locations with N consecutive True values
Python/Numpy/Boolean Indexing: Modify boolean array at locations with N consecutive True values

Time:11-09

Given a numpy boolean array

arr = np.array([1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1])

I would like to indicate where there are at least n consecutive true values (from left to right).

For n = 2:

#         True 2x (or more) in a row
#            /  \        /     \
arr = [1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1]

#                 becomes:

res = [0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0]
#               ^-----------^--^-------- A pattern of 2 or more consecutive True's ends at each of these locations

For n = 3:

#          True 3x (or more) in a row
#                        /     \
arr = [1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1]

#                 becomes:

res = [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]
#                              ^-------- A pattern of 3 or more consecutive True's ends at this location

Is there a pythonic approach to this without using a for loop to iterate over each element? Performance is important here as my application will perform this operation 1000's of times on boolean arrays containing 1000's of elements.

Notes worth mentioning:

  1. n can be any value above 2
  2. n-consecutive patterns can appear anywhere in the array; beginning, middle or end.
  3. The shape of the result array must match that of the original array.

Any help would be very much appreciated.

CodePudding user response:

You can pad/shift the sequence using numpy.pad and compare with the previous value, finally multiply by the original to keep only the 1s:

b = np.pad(arr, (1,0), constant_values=0)[:-1]
# b: array([0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0])
(arr==b)*arr

output: array([0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0])

generic solution:

n = 3
(sum([np.pad(arr, (i, 0), constant_values=0)[:len(arr)]
      for i in range(n)]) == n)*arr

output: array([0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0])

NB. the trick here is to sum the shifted values, if the n previous values are 1, the sum is n

CodePudding user response:

You can multiply each element with its predecessor. This will give you 1s for sequences of two or more. Do it again on the result to get sequences of 3 or more:

import numpy as np
arr = np.array([1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1])

arr[1:] *= arr[:-1]   # two consecutive
arr[0]  = 0           # 1st '1' isn't a two-consec.
arr[1:] *= arr[:-1]   # three consecutive 

print(arr)
[0 0 0 0 0 0 0 0 1 0 0]

You may also try it this way (which is a bit faster):

import numpy as np
arr = np.array([1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1])

arr[2:] *= arr[:-2] * arr[1:-1]
arr[:2] = 0

print(arr)
[0 0 0 0 0 0 0 0 1 0 0]

A generalized solution to n-consecutives could not use the second approach but can be performed in a loop using the 1st one:

n = 3
arr[1:]  *= arr[:-1]
arr[:n-1] = 0
for i in range(n-2):
    arr[1:] *= arr[:-1]

Note that this loses some of the benefits of vectorization.

For a fully vectorized n-consecutive approach, you could select the matching differences between the running maximum of 0 positions with their positions.

n = 3
i = np.arange(arr.size) 1
mask = n <= i-np.maximum.accumulate((1-arr)*i) #True/False array

print(mask*1)
[0 0 0 0 0 0 0 0 1 0 0]

As a side benefit from this approach, you actually get all the n-consecutive values in one operation from i-np.maximum.accumulate((1-arr)*i) so you can store them and check for different values of n without redoing the calculations.

CodePudding user response:

Numpy only:

from numpy.lib.stride_tricks import sliding_window_view

arr = np.array([1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1])
res = sliding_window_view(arr , window_shape = 3).all(axis=1) # window_shape <- n
>>> array([False, False, False, False, False, False,  True, False, False])

  • Related