Robert Christie
Robert Christie

Reputation: 20685

How to detect a sign change for elements in a numpy array

I have a numpy array with positive and negative values in.

a = array([1,1,-1,-2,-3,4,5])

I want to create another array which contains a value at each index where a sign change occurs (For example, if the current element is positive and the previous element is negative and vice versa).

For the array above, I would expect to get the following result

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

Alternatively, a list of the positions in the array where the sign changes occur or list of booleans instead of 0's and 1's is fine.

Upvotes: 37

Views: 44446

Answers (8)

Gwénolé
Gwénolé

Reputation: 45

If you're only interested in change in one direction (below from negative to postive values (positive including zero)):

arr = np.array([1,1,-1,-2,-3,4,5])

np.where(np.diff((arr>=0)*1)==1)

returns the index of every last negative value before change of sign

Upvotes: 0

DougR
DougR

Reputation: 3479

Three methods to determine the location of sign change occurrences

import numpy as np
a = np.array([1,1,-1,-2,-3,4,5])

Method 1: Multiply adjacent items in array and find negative

idx1 = np.where(a[:-1] * a[1:] < 0 )[0] +1
idx1
Out[2]: array([2, 5], dtype=int64)

%timeit np.where(a[:-1] * a[1:] < 0 )[0] + 1
4.31 µs ± 15.1 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

Method 2 (fastest): Where adjacent signs are not equal

idx2 = np.where(np.sign(a[:-1]) != np.sign(a[1:]))[0] + 1
idx2
Out[4]: array([2, 5], dtype=int64)

%timeit np.where(np.sign(a[:-1]) != np.sign(a[1:]))[0] + 1
3.94 µs ± 20.4 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

Method 3: As proposed by ianalis. Most IMO elegant but a little slower

idx3 = np.where(np.diff(np.sign(a)) != 0)[0] + 1
idx3
Out[6]: array([2, 5], dtype=int64)

%timeit np.where(np.diff(np.sign(a)) != 0)[0] + 1
9.7 µs ± 36.2 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

Edit:

For large arrays method 1 is the best.

enter image description here

Upvotes: 16

lhoupert
lhoupert

Reputation: 704

Another idea on getting the 'strict' sign changes from positive to negative and negative to positive (excluding zeros):

a = np.array([0.4, 0.5, -0.2, -0.6, 5, 0, 0, 5, 0,-2])

# Get associated index
ind =np.arange(len(a))

# remove zero from array but keep original index
a2 =a[a!=0.]
ind2  =ind[a!=0.]

# Detect sign changes in reduced array
idx=np.where(np.diff(np.sign(a2)) != 0)[0] + 1

# Get sign changes index for original array
ind2[idx]
array([2, 4, 9])

Upvotes: 1

Christian Alis
Christian Alis

Reputation: 6796

(numpy.diff(numpy.sign(a)) != 0)*1

Upvotes: 29

tom10
tom10

Reputation: 69182

For the direct interpretation of this question, where 0's aren't their own case, it's probably easier to use greater than sign. Here's an example:

a = array([1, 1, -1, -2, -3, 0, 4, 0, 5, 6])

x = greater_equal(a, 0)
sign_change = x[:-1]-x[1:]

Which gives, when printed with T or F to indicate the sign change between different numbers:

 1 F 1 T -1 F -2 F -3 T 0 F 4 F 0 F 5 F 6

when printed using:

print `a[0]`+"".join([(" T" if sign_change[i] else " F")+" "+`a[i+1]` for i in range(len(sign_change))])

Also note that this is one element shorter than the original array, which makes sense since you're asking for the change of sign. If you want to include the change between the last and first element, you can use roll, as others have suggested.

Upvotes: 0

janneb
janneb

Reputation: 37208

Something like

a = array([1,1,-1,-2,-3,4,5])
asign = np.sign(a)
signchange = ((np.roll(asign, 1) - asign) != 0).astype(int)
print signchange
array([0, 0, 1, 0, 0, 1, 0])

Now, numpy.roll does a circular shift, so if the last element has different sign than the first, the first element in the signchange array will be 1. If this is not desired, one can of course do a simple

signchange[0] = 0

Also, np.sign considers 0 to have it's own sign, different from either positive or negative values. E.g. the "signchange" array for [-1,0,1] would be [0,1,1] even though the zero line was "crossed" only once. If this is undesired, one could insert the lines

sz = asign == 0
while sz.any():
    asign[sz] = np.roll(asign, 1)[sz]
    sz = asign == 0

between lines 2 and 3 in the first example.

Upvotes: 38

Personman
Personman

Reputation: 2323

How about

[0 if x == 0 else 1 if numpy.sign(a[x-1]) != numpy.sign(y) else 0 for x, y in enumerate(a)]

numpy.sign assigns 0 its own sign, so 0s will be sign changes from anything except other 0s, which is probably what you want.

Upvotes: 1

Jon
Jon

Reputation: 10473

The answers above use list comprehensions and some numpy magic to get the result you want. Here is a very straight forward, if a little convoluted, way of doing the same:

import numpy as np

arr = np.array([1,1,-1,-2,-3,4,5])

result = []
for i, v in enumerate(arr):
    if i == 0:
        change = False
    elif v < 0 and arr[i-1] > 0:
        change = True
    elif v > 0 and arr[i-1] < 0:
        change = True
    else:
        change = False

    result.append(change)

print result

Upvotes: 1

Related Questions