Efficiently detect sign-changes in python

What about:

import numpy
a = [1, 2, 1, 1, -3, -4, 7, 8, 9, 10, -2, 1, -3, 5, 6, 7, -10]
zero_crossings = numpy.where(numpy.diff(numpy.sign(a)))[0]

Output:

> zero_crossings
array([ 3,  5,  9, 10, 11, 12, 15])

I.e., zero_crossings will contain the indices of elements before which a zero crossing occurs. If you want the elements after, just add 1 to that array.


Another way to count zero crossings and squeeze just a few more milliseconds out of the code is to use nonzero and compute the signs directly. Assuming you have a one-dimensional array of data:

def crossings_nonzero_all(data):
    pos = data > 0
    npos = ~pos
    return ((pos[:-1] & npos[1:]) | (npos[:-1] & pos[1:])).nonzero()[0]

Alternatively, if you just want to count the zero crossings for a particular direction of crossing zero (e.g., from positive to negative), this is even faster:

def crossings_nonzero_pos2neg(data):
    pos = data > 0
    return (pos[:-1] & ~pos[1:]).nonzero()[0]

On my machine these are a bit faster than the where(diff(sign)) method (timings for an array of 10000 sine samples containing 20 cycles, 40 crossings in all):

$ python -mtimeit 'crossings_where(data)'
10000 loops, best of 3: 119 usec per loop

$ python -mtimeit 'crossings_nonzero_all(data)'
10000 loops, best of 3: 61.7 usec per loop

$ python -mtimeit 'crossings_nonzero_pos2neg(data)'
10000 loops, best of 3: 55.5 usec per loop

As remarked by Jay Borseth the accepted answer does not handle arrays containing 0 correctly.

I propose using:

import numpy as np
a = np.array([-2, -1, 0, 1, 2])
zero_crossings = np.where(np.diff(np.signbit(a)))[0]
print(zero_crossings)
# output: [1]

Since a) using numpy.signbit() is a little bit quicker than numpy.sign(), since it's implementation is simpler, I guess and b) it deals correctly with zeros in the input array.

However there is one drawback, maybe: If your input array starts and stops with zeros, it will find a zero crossing at the beginning, but not at the end...

import numpy as np
a = np.array([0, -2, -1, 0, 1, 2, 0])
zero_crossings = np.where(np.diff(np.signbit(a)))[0]
print(zero_crossings)
# output: [0 2]