-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
Add a 2-arg flavor, argmax(f, domain) #27613
Comments
If we have #27612, then this is as simple as defining the following:
(but I haven't checked what to do about |
A more correct definition would be to adapt the current definitions of Base.argmax(f, x::AbstractArray; dims=:) = findmax(f, x, dims=dims)[2]
Base.argmax(f, itr) = findmax(f, itr)[2] with the addition of Base.argmax(x::AbstractArray; dims=:) = findmax(identity, x, dims=dims)[2]
Base.argmax(itr) = findmax(identity, itr)[2] and likewise for |
Thanks very much for taking a look at this. I think you're right. Would the compiler be able to inline the |
Yep. This is the same pattern we use in a lot of places where we define methods that accept function arguments. |
Are there any other functions that could be prime candidates to get the function argument improvement? Besides, |
Wait, I think there are some subtleties in reconciling @ararslan's nice suggestion with my original intent. Let me try to explain, and please forgive me if I confuse things further (e.g. I'm not entirely sure how to talk about the indices of an AbstractArray, but I gave it a shot!) One of the difficulties in thinking about this is that there are really two flavors of In reducedim.jl, I think @ararslan's suggestion is distinct from this. If I understand correctly, for |
We may be talking past each other, but I'll try to clarify. For any iterable |
I think @ararslan’s is the standard Julian extension of a 1-argument function on collections to the case with an additional function argument. But @drewrobson is proposing the traditional 2-arg mathematical argmax, which is less consistent with the existing |
Yes exactly: I completely agree that with the former name, it was clear that With the rename to In summary, I think this is a backwards compatible and satisfying reinterpretation of what this function should do, but I get that it feels like a radical proposal! |
Ohhh okay, I see now, thanks @jekbradbury and @drewrobson for clarifying. An unfortunate consequence of the renaming of |
Yes, avoiding confusion is important. But then, perhaps Edit: @Nosferican's post has reminded me that in addition to the 2-arg form proposed above, we would also keep the 1-arg |
My ignorant self doesn't know what is the appropriate abstract type for an iterate. |
@Nosferican, note that with #27612 this becomes a 1-liner. In other words, first define
and then define
|
Can you elaborate on what you mean by that? |
I may have misinterpreted the above as historical background on why |
@Nosferican, I think you're talking about the mathematical definition of |
Shouldn't be multi-taking. Sorry for the confusion (I suffered it the worst);
rather than the current behavior which is |
There we go. Yes, that is consistent with my original proposal above. |
I'd be interested to hear @stevengj's thoughts on this. |
And perhaps @JeffBezanson and @StefanKarpinski as well? Stefan encouraged me to look into this on Discourse and I know Jeff has seen my other issue at least. Thanks, and sorry to rock the boat! I mean well. |
I think the bug was an oversight when renaming the function from |
I don't see a problem with the single-argument |
I like that. That is consistent with what I was trying to say here: "As a special case, we would preserve the existing 1-arg flavor for things that can be indexed, with the understanding that But the remaining question is, what should be the result of the 2-arg form:
I think @Nosferican and I are arguing that it should be 4, the arg / domain value in Edit: If we go down the latter road then I think we'd want separate verbs |
Long-hand form,
Refresher: Wikipedia |
@Nosferican is right to bring up the issue of ties. I have to sheepishly admit I was trying to avoid that because for efficiency reasons I wanted to do the same thing as |
A compromise I would be willing to accept is for ordered collections to return a
For |
I really like the current behavior of |
When in doubt, drop a keyword argument... If one only cares about a single element you could implement the easy/efficient way and the faithful mathematical function when that is what you want. |
I guess, but together with |
Using Ultimately, I think it'd be better to create a function (say)
Shouldn't we aim for "implicit" handling? It's consistent with the policy to be reluctant to add
We already have |
@StefanKarpinski Thinking about this some more, I agree with @tkf that We want NaN to poison, but it just doesn't dominate all other values (NaN is not less than any other value, but other values aren't less than NaN either), so they will not be consistently chosen by any rule of the type "not less than any other value". I also belatedly realised that floats are not partially ordered because Here's a quick worked example of why # In findmax somewhere:
if fm < fx
fm, m = fx, x
end
# true if fm is less than fx
# good
# false if fm >= fx
# good
# false if fm is NaN
# good
# false if fx is NaN
# not good: we won't switch from a non NaN to a NaN The existing min and max functions all define a preference order for poison values. I think it's a shame that they mostly use I think rather than introduce an is_unorderable(x) = !isa(x ≤ x, Bool) || !(x ≤ x)
poison_lt(a, b) = is_unorderable(a) || is_unorderable(b) ? isless(a, b) : a < b
poison_gt(a, b) = is_unorderable(a) || is_unorderable(b) ? isless(a, b) : a > b
# or a higher order function, whatever
poisoning_order(op) = (a, b) -> is_unorderable(a) || is_unorderable(b) ? isless(a, b) : op(a, b) That way, partial ordering is permitted and we don't introduce a new canonical total ordering into Julia. We could then use these in all of the min and max functions. If that's too big a change or otherwise undesirable, then I think we should define isgreater(a, b) = is_unorderable(a) || is_unorderable(b) ? isless(a, b) : isless(b, a) Anyway, depending on what people are willing to accept, the implementation is either this: findmin(f, domain) = mapfoldl(x -> (f(x), x), _rf_findmin, domain)
_rf_findmin((fm, m), (fx, x)) = !poison_gt(fm, fx) ? (fm, m) : (fx, x)
findmax(f, domain) = mapfoldl(x -> (f(x), x), _rf_findmax, domain)
_rf_findmax((fm, m), (fx, x)) = !poison_lt(fm, fx) ? (fm, m) : (fx, x) or this: findmin(f, domain) = mapfoldl(x -> (f(x), x), _rf_findmin, domain)
_rf_findmin((fm, m), (fx, x)) = isless(fm, fx) ? (fm, m) : (fx, x)
findmax(f, domain) = mapfoldl(x -> (f(x), x), _rf_findmax, domain)
_rf_findmax((fm, m), (fx, x)) = isgreater(fm, fx) ? (fm, m) : (fx, x) Edit: these definitions of findmax return identical results except for types like Sets, for those the first definitions find the first value that is not smaller than all other results. The definition with isgreater just gives an error. |
OK, I agree that there is a way to use
I see that |
I vote yes. If we support partial order in one, we should support it in all of the others. So, perhaps that is beyond the scope of this issue and I should just submit a PR that adds the 2-arg versions of arg* and find* and another that changes all of the min/max functions to support types with partial orders.
From that wikipedia article:
I intended to follow that definition of maximal and I think I did?
Supporting partial orders makes no difference to the existing supported uses (types with total orderings) and provides new functionality that seems reasonable for types that only have partial orderings. I don't have a use case for this, though. Maybe it's something that nobody actually needs. |
I think the partial vs total order thing comes down to this, do you think this should throw an error? If not, what do you think it should return? min(Set(1), Set(2), Set(1:3)) If it should return something, then so too should all the other functions we're talking about. |
Yes, but I think that's the problem; i.e., In other words, a maximal is not (necessary) the maximum in the definition mentioned in Wikipedia (which I think is pretty standard). So, if you take the definition literally, returning a maximal from a function named |
Sorry for the misunderstanding! I wondered if we should define a In any case, if there is a maximum or greatest element, then it is also the only maximal. If there are multiple maximals, then we can (and already have, for findmax) just say that we'll give the first one. I think a line like "If there are multiple maximal elements, then the first one is returned" is pretty clear and better than these functions just erroring on partially ordered types, but I can see your perspective too: we are no longer guaranteeing that the element is the maximum element (though it sort of wasn't before because of NaN and missing anyway). My intention is to submit a PR that adds the 2-arg variants of find* and arg* and ask for triage on whether these functions should error or return the first maximal for types that can only be partially ordered. Does that sound good to you? |
Yes, I can see that a function like this may be useful sometimes. At the same time, I think having a value guaranteed to be the maximum is also useful, too (e.g., using it as a sentinel value).
Current behavior of By the way, another problem for julia> maximum([-0.0, 0.0])
0.0
julia> foldl([-0.0, 0.0]) do a, b
a < b ? b : a
end
-0.0 |
Maybe the original sin here is that I believed the docstring for I think that that probably merits a documentation change, and, if we wanted to take partial orders seriously then there should probably be a new function for the default partial order relation on a type (and we'd need to decide what it should do for unorderables and missings: error or put them at the end).
Thanks for pointing that out. I basically just need to finalise the docstrings for this and check that isgreater isn't hilariously wrong and I'll submit the PR for review, probably tomorrow. I'll be editing the docstrings of |
I haven't read all the rest here, but that's exactly why the condition to update the max is that the max is less than the new value: because |
The issue is that if the current max is say 3.0, then we won't switch to NaN because 3.0 < NaN is false. |
If floats were partially ordered it would be fine, but they're not, NaNs are unordered and will only be picked as the max if they're the first item in the iterable. |
That's a fair point.
Yeah, it's true that this behavior is not in the docstring but it doesn't mean changing it is OK. (In a way "true" SemVer is a fantasy...) It is already documented that |
I think it's defined somewhere or other that negative zero isless positive zero. If I can't find where then I'll add it to the docstring in implementer notes or something. |
Fixes JuliaLang#27613. Related: JuliaLang#27639, JuliaLang#27612, JuliaLang#34674. Thanks to @tkf, @StefanKarpinski and @drewrobson for their assistance with this PR.
Fixes JuliaLang#27613. Related: JuliaLang#27639, JuliaLang#27612, JuliaLang#34674. Thanks to @tkf, @StefanKarpinski and @drewrobson for their assistance with this PR.
Fixes JuliaLang#27613. Related: JuliaLang#27639, JuliaLang#27612, JuliaLang#34674. Thanks to @tkf, @StefanKarpinski and @drewrobson for their assistance with this PR.
I finally submitted that PR. It's here: #35316. 🎉 |
Fixes JuliaLang#27613. Related: JuliaLang#27639, JuliaLang#27612, JuliaLang#34674. Thanks to @tkf, @StefanKarpinski and @drewrobson for their assistance with this PR.
Fixes JuliaLang#27613. Related: JuliaLang#27639, JuliaLang#27612, JuliaLang#34674. Thanks to @tkf, @StefanKarpinski and @drewrobson for their assistance with this PR.
Defines a descending total order, `isgreater` (not exported), where unordered values like NaNs and missing are last. This makes defining min, argmin, etc, simpler and more consistent. Also adds 2-arg versions of findmax/min, argmax/min. Defines and exports the `isunordered` predicate for testing whether a value is unordered like NaN and missing. Fixes #27613. Related: #27639, #27612, #34674. Thanks to @tkf, @StefanKarpinski and @drewrobson for their assistance with this PR. Co-authored-by: Jameson Nash <[email protected]> Co-authored-by: Takafumi Arakaki <[email protected]>
Defines a descending total order, `isgreater` (not exported), where unordered values like NaNs and missing are last. This makes defining min, argmin, etc, simpler and more consistent. Also adds 2-arg versions of findmax/min, argmax/min. Defines and exports the `isunordered` predicate for testing whether a value is unordered like NaN and missing. Fixes JuliaLang#27613. Related: JuliaLang#27639, JuliaLang#27612, JuliaLang#34674. Thanks to @tkf, @StefanKarpinski and @drewrobson for their assistance with this PR. Co-authored-by: Jameson Nash <[email protected]> Co-authored-by: Takafumi Arakaki <[email protected]>
With the rename to argmax, and dims now a kwarg, it seems very tempting to me to define a 2-arg flavor,
argmax(f, itr)
, whereitr
is an iterable subset of the domain off
. This would return anx
initr
that maximizesf(x)
, which is quite literally the mathematical definition of argmax.(originally proposed here)
Edit: Previous wording may have been confusing, so I have attempted to improve it.
The text was updated successfully, but these errors were encountered: