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