Efficiently detect sign-changes in python
Solution 1:
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.
Solution 2:
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]
Solution 3:
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
Solution 4:
Jim Brissom's answer fails if a contains the value 0:
import numpy
a2 = [1, 2, 1, 1, 0, -3, -4, 7, 8, 9, 10, -2, 1, -3, 5, 6, 7, -10]
zero_crossings2 = numpy.where(numpy.diff(numpy.sign(a2)))[0]
print zero_crossings2
print len(zero_crossings2) # should be 7
Output:
[ 3 4 6 10 11 12 13 16]
8
The number of zero crossing should be 7, but because sign() returns 0 if 0 is passed, 1 for positive, and -1 for negative values, diff() will count the transition containing zero twice.
An alternative might be:
a3 = [1, 2, 1, 1, 0, -3, -4, 7, 8, 9, 10, 0, -2, 0, 0, 1, 0, -3, 0, 5, 6, 7, -10]
s3= numpy.sign(a3)
s3[s3==0] = -1 # replace zeros with -1
zero_crossings3 = numpy.where(numpy.diff(s3))[0]
print s3
print zero_crossings3
print len(zero_crossings3) # should be 7
which give the correct answer of:
[ 3 6 10 14 15 18 21]
7