Skip to content

Enhance piecewise to allow functions to have arbitrary number of input arguments #5187

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 0 commits into from

Conversation

pbrod
Copy link
Contributor

@pbrod pbrod commented Oct 14, 2014

Made piecewise function simpler and more general. The new 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 old piecewise function did allow condlist to be a single bool list/array or a single int array
(That feature was not documented in the docstring of the function, but there were tests testing this functionality.) Question is if this feature should be deprecated. For now this feature is kept in the new version.

See also: http://www.mail-archive.com/numpy-discussion@scipy.org/msg46304.html

Examples

Examples that work both in previous and current version

>>> x = np.linspace(-2,2,5)
>>> np.piecewise(x, [x<1], [1]) 
array([ 1.,  1.,  1.,  0.,  0.])

>>> np.piecewise((x,), [x < 0, x >= 0], [lambda x: -x, lambda x: x])
array([ 2.,  1.,  0.,  1.,  2.])

can be written as

>>> np.piecewise(x,[x < 0, x >= 0], [lambda x: -x, lambda x: x])
array([ 2.,  1.,  0.,  1.,  2.])

or

>>> np.piecewise(x, [x < 0,], [lambda x: -x, lambda x: x])
array([ 2.,  1.,  0.,  1.,  2.])

Example that still works but should possibly be deprecated:

>>> np.piecewise(x, x < 0, [lambda x: -x, lambda x: x])
array([ 2.,  1.,  0.,  1.,  2.])

Examples demonstrating the new functionality

>>> X,Y = np.meshgrid(x,x)
>>> np.piecewise((X,Y), [X*Y<0,], [lambda x,y: -x*y, lambda x,y: 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.]])

>>> np.piecewise((X,Y),[X*Y<-0.5, X*Y>0.5], [lambda x,y: -x*y, lambda x,y: x*y, np.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.]])

@charris
Copy link
Member

charris commented Jan 23, 2015

We can't break backward compatibility here. Either a new function is needed, or the enhanced function needs to be fixed to work with old arguments.

@pbrod
Copy link
Contributor Author

pbrod commented Jan 27, 2015

According to the docstring of the old function:

 condlist : list of bool arrays
        Each boolean array corresponds to a function in `funclist`.  Wherever
    `    condlist[i]` is True, `funclist[i](x)` is used as the output value.

Thus the example

x = np.linspace(-2,2,5)
np.piecewise(x, x < 0, [lambda x: -x, lambda x: x])

violates the condition that condlist should be a list. Here condlist is a ndarray. The old function allowed this calling syntax, but it is clearly an undocumented feature of the old function. The question is if that is a wanted behavior?

For now I have allowed this behavior and removed any backward incompatibilties as far as I know. The only thing I have added is a deprecation warning if the condlist is not a list of booleans or boolean arrays.

@homu
Copy link
Contributor

homu commented Jun 20, 2015

☔ The latest upstream changes (presumably #5985) made this pull request unmergeable. Please resolve the merge conflicts.

@pbrod
Copy link
Contributor Author

pbrod commented Jul 13, 2015

This version of the 'piecewice' function resolves the problems mentioned in ticket #5729 and #5737 as well as #4792.

Added regression tests for #5729 and #5737.

@homu
Copy link
Contributor

homu commented Jul 26, 2015

☔ The latest upstream changes (presumably #6115) made this pull request unmergeable. Please resolve the merge conflicts.

@homu
Copy link
Contributor

homu commented Feb 1, 2016

☔ The latest upstream changes (presumably #7145) made this pull request unmergeable. Please resolve the merge conflicts.

@hunterakins
Copy link

Seems this didn't merge...would be a useful enhancement. Any update on this?

@@ -838,7 +838,19 @@ class ndarray is returned.
return a


def piecewise(x, condlist, funclist, *args, **kw):
def valarray(shape, value=0.0, typecode=None):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is just a less reliable np.full - and it looks like it's not used anyway

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain how you can substitute np.piecewise with np.full?
Or, to avoid the xy problem: How would one write a conditional function that works cleanly with both 1-d numpy arrays as well as the 2-d array outputs from meshgrid?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm commenting on valarray, not piecewise

@eric-wieser
Copy link
Member

@hunterakins: The problem is that piecewise((x1, x2. x3), ...) is already valid syntax, and is the same as np.piecewise(np.array([x1, x2, x3])). We can't change that behavior without at least a deprecation cycle.

The best way forward would be to introduce a FutureWarning when x is a tuple, and then maybe in 1.18 the new behavior can be introduced

@hunterakins
Copy link

hunterakins commented Oct 31, 2018 via email

@anirudh2290
Copy link
Member

@pbrod This has been open for a while. Do you plan to come back to this ?

@pbrod
Copy link
Contributor Author

pbrod commented May 21, 2020

@anirudh2290 Well, I am happy to complete this, but I need guidance on how to proceed.

@anirudh2290
Copy link
Member

@pbrod According to me, looks like this enhancement would be a good to have.
As for the further direction, as @eric-wieser suggests we should first deprecate the tuple input which is just converted to a single ndarray and add a FutureWarning for the same. We can add this new behavior after 2 releases.

Thus the example

x = np.linspace(-2,2,5)
np.piecewise(x, x < 0, [lambda x: -x, lambda x: x])
violates the condition that condlist should be a list. Here condlist is a ndarray. The old function
allowed this calling syntax, but it is clearly an undocumented feature of the old function. The > question is if that is a wanted behavior?

Since single condition is promoted to a list of one condition, I won't mind keeping it as it is and documenting this behavior.

cc @eric-wieser for a second look.

@eric-wieser
Copy link
Member

Can we come up with a new spelling that doesn't overlap the old one?

As a reminder, we currently have:

np.piecewise(
    arr_like,
    [cond1, cond2, cond3],
    [func1, func2, func3, func_default],
    *func_args, **func_kwargs)

One option might be pulling some magic with __getitem__:

np.piecewise[2](
    arr_like1,
    arr_like2,
    [cond1, cond2, cond3],
    [func1, func2, func3, func_default],
    *func_args, **func_kwargs)

or by currying (noting that 2-argument invocations are currently not allowed):

np.piecewise(
    [cond1, cond2, cond3],
    [func1, func2, func3, func_default])(arr_like1, arr_like2)

Will edit this post if I think of more...

Copy link
Member

@eric-wieser eric-wieser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like there are more behavior changes here than I first spotted


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(x,*args,**kw), or scalars
funclist : list of callables, f(*(xi + args), **kw), or scalars
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
funclist : list of callables, f(*(xi + args), **kw), or scalars
funclist : list of callables, f(*xi, *args, **kw), or scalars

is also legal and probably clearer

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

xi may be slightly misleading since these funcs run on indexed array as such which will be 1d and may not be all elements of xi.

Comment on lines 994 to 995
# corresponding condlist as a boolean mask. This is done in reverse
# order since the first choice should take precedence.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

xref #16475, this is a behavior change too

Comment on lines 978 to 982
if deprecated_ints:
msg = "piecewise condlists containing integer ndarrays is deprecated " \
"and will be removed in the future. Use `.astype(bool)` to " \
"convert to bools."
warnings.warn(msg, DeprecationWarning)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is probably sensible to extract to its own PR. Note this is missing stacklevel=2.

assert_(y.ndim == 0)
assert_(y == 0)

x = 5
y = piecewise(x, [[True], [False]], [1, 0])
assert_(y.ndim == 0)
assert_(y.ndim == 1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change as well sadly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants