15

As the title says, suppose I want to write a sign function (let's forget sign(0) for now), obviously we expect sign(2) = 1 and sign(array([-2,-2,2])) = array([-1,-1,1]). The following function won't work however, because it can't handle numpy arrays.

def sign(x):
    if x>0: return 1
    else: return -1

The next function won't work either since x doesn't have a shape member if it's just a single number. Even if some trick like y = x*0 + 1 is used, y won't have a [] method.

def sign(x):
    y = ones(x.shape)
    y[x<0] = -1
    return y

Even with the idea from another question(how can I make a numpy function that accepts a numpy array, an iterable, or a scalar?), the next function won't work when x is a single number because in this case x.shape and y.shape are just () and indexing y is illegal.

def sign(x):
    x = asarray(x)
    y = ones(x.shape)
    y[x<0] = -1
    return y

The only solution seems to be that first decide if x is an array or a number, but I want to know if there is something better. Writing branchy code would be cumbersome if you have lots of small functions like this.

Community
  • 1
  • 1
Taozi
  • 373
  • 5
  • 13
  • 1
    Indexing `y` with a mask *is* legal: the problem here is that `x < 0` is again a scalar rather than a 0-d array. If you try `y[asarray(x < 0)]` it should work. – Mark Dickinson Oct 24 '14 at 07:00
  • Have you considered the possibility of using the built in `np.sign`? – Jaime Oct 24 '14 at 08:15
  • @MarkDickinson This is a good one, but it gives errors when x is a single number because now y must be a single number as well -- then y can't be indexed... – Taozi Oct 24 '14 at 14:57
  • @Jaime The sign function is just for example, I should have used something like a piecewise function in math. BTW do you know how sign function is implemented in numpy? – Taozi Oct 24 '14 at 14:59
  • @Taozi: Right; you'd still need the `x = asarray(x)` at the beginning if you wanted to take this approach. – Mark Dickinson Oct 24 '14 at 16:14

8 Answers8

4

np.vectorize can be used to achieve that, but would be slow because all it does, when your decorated function is called with an array, is looping through the array elements and apply the scalar function to each, i.e. not leveraging numpy's speed.

A method I find useful for vectorizing functions involving if-else is using np.choose:

def sign_non_zero(x):
    return np.choose(
        x > 0,  # bool values, used as indices to the array
        [
            -1, # index=0=False, i.e. x<=0
            1,  # index=1=True, i.e. x>0
        ])

This works when x is either scalar or an array, and is faster than looping in python-space.

The only disadvantage of using np.choose is that it is not intuitive to write if-else logic in that manner, and the code is less readable. Whenver I use it, I include comments like the ones above, to make it easier on the reader to understand what is going on.

shx2
  • 61,779
  • 13
  • 130
  • 153
  • >>> sign_non_zero([1,2,3]) gives 1 # should have been 1,1,1 >>> sign_non_zero([1,2,-3]) gives 1 # should have been 1,1,-1 – Irshad Bhat Oct 24 '14 at 07:25
  • @BHATIRSHAD, right, as currently stands, `sign_non_zero` supports scalars and numpy arrays. To also support lists, you can simply replace `x` with `np.asarray(x)`. – shx2 Oct 24 '14 at 08:00
  • 1
    @bhat-irshad This one is perfect for implementing the sign function after x is replaced with np.asarray(x). However using choose seems only convenient when the result is yes-or-no. If you have a threefold decision to make (suppose you consider sign(0)) now, then choose function is useless and we have to face the old question again -- if x can be indexed, aka, if x is a number or an array. – Taozi Oct 24 '14 at 15:12
  • @Taozi I wouldn't say *choose is useless* if you have a threefold decision to make. Each `if` or `elif` statement in your "scalar-version" can translate to a call to `choose`, so you'd need to use `choose` twice. The complexity of your scalar-version, in terms of number of if-statemetns, is preserverd in your choose-version, in terms of number of choose-calls. This still has the advatage of being numpy-fast. – shx2 Oct 31 '14 at 07:38
  • @shx2 I would conclude that what I want to achieve is impossible. That's fine actually since most of the time it's very clear whether you are dealing with array or not. The question comes from when I was playing with the exponential distribution f(x) = {0:(x<0); exp(-x):(otherwise)} which is piecewise. I would deal with it the hard way. – Taozi Oct 31 '14 at 20:21
  • 1
    nice use of `np.choose`. You can also take a look at `np.select` for more complex return types – goofd Dec 01 '14 at 18:09
3

i wonder if it's a vectorized function that you want:

>>> import numpy as NP

>>> def fnx(a):
        if a > 0:
            return 1
        else:
            return -1

>>> vfnx = NP.vectorize(fnx)

>>> a = NP.random.randint(1, 10, 5)
array([4, 9, 7, 9, 2])

>>> a0 = 7

>>> vfnx(a)
array([1, 1, 1, 1])

>>> vfnx(a0)
array(1)
doug
  • 69,080
  • 24
  • 165
  • 199
  • This is nice, but is it true as mentioned by shx2 that vectorized function is slow and don't take advantage of numpy's speed? Also if this method used, each function needs to be defined twice - one is the humble version that focus on a single number, one is the vectorized version whose name should be close but different, is this correct? – Taozi Oct 24 '14 at 15:30
  • according to the docs, a vectorized fn is implemented as a python for loop and indeed a major reason for NumPy's performance is array-oriented computation (just a single for loop in the C source) that avoids the second python for loop. But you do not need a second fn; the purpose of a vectorized fn is to handle both NumPy arrays and scalars using the same fn in a single fn call. – doug Nov 01 '14 at 06:01
3

Here's one solution:

import numpy as np

def sign(x):
    y = np.ones_like(x)
    y[np.asarray(x) < 0] = -1

    if isinstance(x, np.ndarray):
        return y
    else:
        return type(x)(y)

This should return a value of the same type as the input. For example sign(42) gives 1, sign(42.0) gives 1.0. If you give it an ndarray, it will work like np.sign.

In general, you might proceed with the assumption that your input is an ndarray. If you try to access an attribute or method that an ndarray has, but your input does not, then you fall back to operating on a scalar type. Use exceptions to implement this. For example:

def foo_on_scalars(x):
    # do scalar things

def foo(x):
    try:
        # assume x is an ndarray
    except AttributeError:
        foo_on_scalars(x)
jme
  • 19,895
  • 6
  • 41
  • 39
1

The numpy functions naturally handle scalar or array inputs and preserve the shape in the output. So, it's always best to find the numpy functions doing the job. In this case, the function should be np.sign as suggested above. For different logics, you can use np.where(x>0, 1, -1), which works for scalar and array values of x.

jChoi
  • 151
  • 6
0

you can convert the number to a single-element array first,

and then concentrate on operating on arrays.

you still have to check the type of x though

adrianX
  • 619
  • 7
  • 21
  • But then the function is returning a single element array which needs unpacking from the client side. – Taozi Oct 24 '14 at 15:25
0

Here is one solution:

>>> def sign(x):
...      if type(x)==int:
...          if x>0: return 1
...          else: return -1 
...      else:
...          x=np.array(x)
...          pos=np.where(x>=0)
...          neg=np.where(x<0)
...          res=np.zeros(x.shape[0])
...          res[pos]=1
...          res[neg]=-1
...          return res.tolist()
... 
>>> sign(56)
1
>>> sign(-556)
-1
>>> sign([23,4,-3,0,45,-3])
[1.0, 1.0, -1.0, 1.0, 1.0, -1.0]
>>> sign(np.array([23,4,-3,0,45,-3]))
[1.0, 1.0, -1.0, 1.0, 1.0, -1.0]
Irshad Bhat
  • 8,479
  • 1
  • 26
  • 36
  • 1
    what would be the output of `sign(56L)`? or `sign(np.int32(56))`? `sign(56.)`? Besides, the whole point is to avoid the dupcliation of logic. – shx2 Oct 24 '14 at 07:06
  • @shx2 Exactly what I want to ask, the problem with type judging is that there are so many types. It's possible to use if type(numpyarray) == 'ndarray', but those branches are all that I want to avoid. – Taozi Oct 24 '14 at 15:24
0

The approach I have taken before is alot like your last example, but adding an additional check for scalars at the beginning:

def sign(x):
    if isscalar(x):
        x = (x,)
    x = asarray(x)
    y = ones(x.shape)
    y[x<0] = -1
    return y
dfreese
  • 378
  • 1
  • 10
0

Simple solution that handles scalars and numpy arrays:

>>> import numpy as np

>>> def sign_non_zero(x):
        return (x > 0) * 1 + (x < 0) * -1

>>> sign_non_zero(2)
1

>>> sign_non_zero(np.array([-2, -2, 2]))
array([-1, -1,  1])
jared
  • 56
  • 2