[Numpy-discussion] Any interest in a generalized piecewise function?

Per.Brodtkorb at ffi.no Per.Brodtkorb at ffi.no
Fri Oct 10 05:34:17 EDT 2014


I have worked on a generalized piecewise function (genpiecewise) that are simpler and more general than the current numpy.piecewise implementation. The new generalized piecewise function allows functions of the type f(x0, x1,.. , xn) i.e. to have arbitrary number of input arguments that are evaluated conditionally.
The generalized piecewise function passes all the tests for numpy.piecewise function except the undocumented features of numpy.piecewise which allows condlist to be a single bool list/array or a single int array.

A new keyword "fillvalue" opens up the possibility to specify "out of bounds values" to other values than 0 eg. Np.nan.

Examples:
Example 1)
>>> x = numpy.linspace(-2,2,5)
>>> numpy.piecewise(x, x<1, [1])  # = numpy.where(x<0, 1, 0)
array([ 1.,  1.,  1.,  0.,  0.])

# can be written as
>>> genpiecewise([x<1],[1])
array([1, 1, 1, 0, 0])
# or
>>> genpiecewise([x<1],[1], x)
array([ 1.,  1.,  1.,  0.,  0.])
# or
>>> genpiecewise([x<1],[1.0])
array([ 1.,  1.,  1.,  0.,  0.])

Example 2)
>>> numpy.piecewise(x, [x < 0, x >= 0], [lambda x: -x, lambda x: x])
array([ 2.,  1.,  0.,  1.,  2.])
# can be written as
>>> genpiecewise([x < 0, x >= 0], [lambda x: -x, lambda x: x], (x,))
array([ 2.,  1.,  0.,  1.,  2.])
# or
genpiecewise([x < 0,], [lambda x: -x, lambda x: x], x)
array([ 2.,  1.,  0.,  1.,  2.])

Example 3)
# New functionality
>>> X,Y = numpy.meshgrid(x,x)
>>> genpiecewise([X*Y<0,], [lambda x,y: -x*y, lambda x,y: x*y], xi=(X,Y))
array([[ 4.,  2., -0.,  2.,  4.],
       [ 2.,  1., -0.,  1.,  2.],
       [-0., -0.,  0.,  0.,  0.],
       [ 2.,  1.,  0.,  1.,  2.],
       [ 4.,  2.,  0.,  2.,  4.]])
>>> genpiecewise([X*Y<-0.5, X*Y>0.5], [lambda x,y: -x*y, lambda x,y: x*y], xi=(X,Y), fillvalue=numpy.nan)
array([[  4.,   2.,  nan,   2.,   4.],
       [  2.,   1.,  nan,   1.,   2.],
       [ nan,  nan,  nan,  nan,  nan],
       [  2.,   1.,  nan,   1.,   2.],
       [  4.,   2.,  nan,   2.,   4.]])
>>> genpiecewise([X*Y<-0.5, X*Y>0.5], [lambda x,y: -x*y, lambda x,y: x*y, numpy.nan], (X,Y))
array([[  4.,   2.,  nan,   2.,   4.],
       [  2.,   1.,  nan,   1.,   2.],
       [ nan,  nan,  nan,  nan,  nan],
       [  2.,   1.,  nan,   1.,   2.],
       [  4.,   2.,  nan,   2.,   4.]])

My question is: are there any interest in the community for such a function?
Could some or all of this functionality replace the current numpy.piecewise?
(This function could replace the function _lazywhere (in scipy.stats._distn_infrastructure) which is heavily used in scipy.stats)


Per A. Brodtkorb

The code looks like this:

def valarray(shape, value=np.nan, typecode=None):
    """Return an array of all value.
    """

    out = ones(shape, dtype=bool) * value
    if typecode is not None:
        out = out.astype(typecode)
    if not isinstance(out, np.ndarray):
        out = asarray(out)
    return out


def genpiecewise(condlist, funclist, xi=None, fillvalue=0, args=(), **kw):
    """
    Evaluate a piecewise-defined function.

    Given a set of conditions and corresponding functions, evaluate each
    function on the input data wherever its condition is true.

    Parameters
    ----------
    condlist : list of bool arrays
        Each boolean array corresponds to a function in `funclist`.  Wherever
        `condlist[i]` is True, `funclist[i](x0,x1,...,xn)` is used as the
        output value. Each boolean array in `condlist` selects a piece of `xi`,
        and should therefore be of the same shape as `xi`.

        The length of `condlist` must correspond to that of `funclist`.
        If one extra function is given, i.e. if
        ``len(funclist) - len(condlist) == 1``, then that extra function
        is the default value, used wherever all conditions are false.
    funclist : list of callables, f(*(xi + args), **kw), or scalars
        Each function is evaluated over `x` wherever its corresponding
        condition is True.  It should take an array as input and give an array
        or a scalar value as output.  If, instead of a callable,
        a scalar is provided then a constant function (``lambda x: scalar``) is
        assumed.
    xi : tuple
        input arguments to the functions in funclist, i.e., (x0, x1,...., xn)
    fillvalue : scalar
        fillvalue for out of range values. Default 0.
    args : tuple, optional
        Any further arguments given here passed to the functions
        upon execution, i.e., if called ``piecewise(..., ..., args=(1, 'a'))``,
        then each function is called as ``f(x0, x1,..., xn, 1, 'a')``.
    kw : dict, optional
        Keyword arguments used in calling `piecewise` are passed to the
        functions upon execution, i.e., if called
        ``piecewise(..., ..., lambda=1)``, then each function is called as
        ``f(x0, x1,..., xn, lambda=1)``.

    Returns
    -------
    out : ndarray
        The output is the same shape and type as x and is found by
        calling the functions in `funclist` on the appropriate portions of `x`,
        as defined by the boolean arrays in `condlist`.  Portions not covered
        by any condition have undefined values.


    See Also
    --------
    choose, select, where

    Notes
    -----
    This is similar to choose or select, except that functions are
    evaluated on elements of `xi` that satisfy the corresponding condition from
    `condlist`.

    The result is::

            |--
            |funclist[0](x0[condlist[0]],x1[condlist[0]],...,xn[condlist[0]])
      out = |funclist[1](x0[condlist[1]],x1[condlist[1]],...,xn[condlist[1]])
            |...
            |funclist[n2](x0[condlist[n2]],x1[condlist[n2]],...,xn[condlist[n2]])
            |--

    Examples
    --------
    Define the sigma function, which is -1 for ``x < 0`` and +1 for ``x >= 0``.

    >>> x = np.linspace(-2.5, 2.5, 6)
    >>> genpiecewise([x < 0, x >= 0], [-1, 1])
    array([-1., -1., -1.,  1.,  1.,  1.])

    Define the absolute value, which is ``-x`` for ``x <0`` and ``x`` for
    ``x >= 0``.

    >>> genpiecewise([x < 0, x >= 0], [lambda x: -x, lambda x: x], (x,))
    array([ 2.5,  1.5,  0.5,  0.5,  1.5,  2.5])

    """
    def otherwise_condition(condlist):
        return ~np.logical_or.reduce(condlist, axis=0)

    def check_shapes(condlist, funclist):
        nc, nf = len(condlist), len(funclist)
        if nc not in [nf-1, nf]:
            raise ValueError("function list and condition list" +
                             " must be the same length")

    check_shapes(condlist, funclist)
    if xi is not None and not isinstance(xi, tuple):
        xi = (xi,)

    condlist = np.broadcast_arrays(*condlist)
    if len(condlist) == len(funclist)-1:
        condlist.append(otherwise_condition(condlist))

    arrays = dtype = None
    if xi is not None:
        arrays = np.broadcast_arrays(*xi)
        dtype = np.result_type(*arrays)
    else:  # funclist is a list of scalars only
        dtype = np.result_type(*funclist)
    out = valarray(condlist[0].shape, fillvalue, dtype)
    for cond, func in zip(condlist, funclist):
        if isinstance(func, collections.Callable):
            temp = tuple(np.extract(cond, arr) for arr in arrays) + args
            np.place(out, cond, func(*temp, **kw))
        else:  # func is a scalar value
            np.place(out, cond, func)
    return out



-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.python.org/pipermail/numpy-discussion/attachments/20141010/ff64b99a/attachment.html>


More information about the NumPy-Discussion mailing list