You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
>>> x = xp.full(5, xp.nan)
>>> x
Array([nan, nan, nan, nan, nan])
Should NaNs be treated as unique to one another?
>>> xp.unique(x)
Array([nan, nan, nan, nan, nan])
Or should they be treated as the same?
>>> xp.unique(x)
Array([nan])
I have no dog in the race but you might want to refer to some discussion bottom of numpy/numpy#19301 and in the NumPy mailing list that relates to a recent accidental "regression" in how np.unique() deals with NaNs.
In either case, just specificity would prevent creating wrapper methods to standardise this behaviour. For example I created this (admittedly scrappy) utility method for HypothesisWorks/hypothesis#3065 to work for both scenarios, which @asmeurer noted was not ideal and probably a failure of the Array API spec.
IMO this depends on the definition of "unique". If it means "value equality", it should return all occurrences of NaN, since assert float("nan") != float("nan"). If it means "identity", it should return only one NaN, since assert float("nan") is float("nan"). (See #249 (comment))
If I'm not mistaken, in this context "unique" means value equality, since we are dealing with values and not objects (is np.unique defined for the object dtype?). To satisfy both requirements, we could add an equal_nan flag that, if True, treats NaN's as equal. This is similar to what most testing functions do, e.g. np.testing.assert_allclose.
Are there clear use cases for returning multiple nan's? As @thomasjpfan said in numpy/numpy#19301, a single nan is what is expected for ML applications. And in other contexts I'd expect that to be true as well. I can't remember any case where I had a need for multiple distinct nan's.
For PyTorch I'm not aware of a discussion about this. As for the behavior, it seems to use the value equality approach I described in my comment above:
math.nan is math.nan, sure, but otherwise identity of separately-constructed objects is implementation-defined and probably False. The only reliable exceptions are singletons (None, False, True, etc.) and some implementation-defined details like CPython's interning for small integers and strings.
(personally I favor the "return one nan" option, but we'd better document whatever we decide)
TensorFlow and CuPy also return multiple nan's. CuPy will almost certainly change that to match NumPy. For TensorFlow I also could not find an issue. It seems likely that in all libraries the multiple-nan return is just a natural result of implementing unique via comparisons without any special handling, not because it's useful behaviour.
It'd be very annoying to implement on GPUs if we were to return a unique nan. In any case unique relies on sort or argsort, and it creates a thread divergence that hurts performance just to check special cases like this. We were (=I was) bothered enough to follow NumPy's way to handle complex nan ordering, and would prefer to not add any more special cases. (You can imagine it'd only hurt performance more by an additional call to isnan just to mark where the nan starts in a sorted array, count the occurrence so as to allocate output memory, etc)
Hmm, that's a good point. Although it raises the question what users do then, they usually need to handle the nan's anyway by skipping or combining them.
Your argument means adding an equal_nan also doesn't help, then you'd still have to implement it.
Could equal_nan be a kwarg which must support None (i.e. whatever xp.unique() would do by default) but can omit either/both True and False? The spec doesn't allow omission of any specific arguments per-say but it does allow omission of boolean indexing (technically a specified argument for get/set item), so maybe it's not too "magical" of a compromise.
It could even be that a library which knows how it will return NaNs in xp.unique() can allow the respective equal_nan bool arg to just work, and raise a helpful error for the opposite arg.
From a suggestion in the call, what if a unique implementation filtered out nans first? Something like this?
a_notnan=a[~np.isnan(a)]
r=np.unique(a_notnan)
A limitation with this example specifically is that boolean indexing might not be supported by an Array API library (it's optional behaviour in the spec, see #84). You could manually zip an array a with its NaN mask xp.isnan(a) to then construct a unique array, but this could be painfully slow, and generally an awkward implementation (see Leo Fang's previous comment).
In either the proposed unique_all() (#275) or the current unique(..., return_counts=True), I just wanted to clarify what counting behaviour we can expect from both NaNs-unique and NaNs-not-unique scenarios.
I assume that we get the following if NaNs are unique to each-other:
I think we have to make counts and values consistent with one another, regardless of which nan approach we take. It would be nonsensical to return something like values == array([nan, nan, nan, nan, nan]); counts = array([5]). Then values and counts wouldn't even correspond to each other any more.
There would be some additional work for getting the indices, inverse_indices, and counts.
Another workaround for users is to select a sentinel value other than NaN to indicate a "missing" value for the purposes of unique. For example, if values are known to be integers on the interval [1,100], replace NaN values with 0 before calling unique.
In short, the issue of multiple NaN values seems, to me at least, to be a user concern and should thus be pushed to userland. While the utility of multiple NaN values in the output of unique is undoubtedly limited, I don't see why the prevention of this should be the responsibility of array libraries, especially when bearing this responsibility entails a performance cost.
Activity
pmeier commentedon Sep 1, 2021
IMO this depends on the definition of "unique". If it means "value equality", it should return all occurrences of NaN, since
assert float("nan") != float("nan").If it means "identity", it should return only one NaN, since(See #249 (comment))assert float("nan") is float("nan").If I'm not mistaken, in this context "unique" means value equality, since we are dealing with values and not objects (is
np.uniquedefined for theobjectdtype?). To satisfy both requirements, we could add anequal_nanflag that, ifTrue, treats NaN's as equal. This is similar to what most testing functions do, e.g.np.testing.assert_allclose.rgommers commentedon Sep 1, 2021
Are there clear use cases for returning multiple
nan's? As @thomasjpfan said in numpy/numpy#19301, a singlenanis what is expected for ML applications. And in other contexts I'd expect that to be true as well. I can't remember any case where I had a need for multiple distinctnan's.asmeurer commentedon Sep 1, 2021
How do pytorch and tensorflow (and any others) handle this? Do they also have discussions about this behavior?
pmeier commentedon Sep 2, 2021
For PyTorch I'm not aware of a discussion about this. As for the behavior, it seems to use the value equality approach I described in my comment above:
Zac-HD commentedon Sep 4, 2021
math.nan is math.nan, sure, but otherwise identity of separately-constructed objects is implementation-defined and probablyFalse. The only reliable exceptions are singletons (None,False,True, etc.) and some implementation-defined details like CPython's interning for small integers and strings.(personally I favor the "return one
nan" option, but we'd better document whatever we decide)pmeier commentedon Sep 4, 2021
@Zac-HD my bad, thanks for the heads-up. I've fixed my comment above.
rgommers commentedon Sep 5, 2021
TensorFlow and CuPy also return multiple
nan's. CuPy will almost certainly change that to match NumPy. For TensorFlow I also could not find an issue. It seems likely that in all libraries the multiple-nan return is just a natural result of implementinguniquevia comparisons without any special handling, not because it's useful behaviour.leofang commentedon Sep 5, 2021
It'd be very annoying to implement on GPUs if we were to return a unique nan. In any case
uniquerelies onsortorargsort, and it creates a thread divergence that hurts performance just to check special cases like this. We were (=I was) bothered enough to follow NumPy's way to handle complex nan ordering, and would prefer to not add any more special cases. (You can imagine it'd only hurt performance more by an additional call toisnanjust to mark where the nan starts in a sorted array, count the occurrence so as to allocate output memory, etc)rgommers commentedon Sep 6, 2021
Hmm, that's a good point. Although it raises the question what users do then, they usually need to handle the nan's anyway by skipping or combining them.
Your argument means adding an
equal_nanalso doesn't help, then you'd still have to implement it.honno commentedon Sep 6, 2021
Could
equal_nanbe a kwarg which must supportNone(i.e. whateverxp.unique()would do by default) but can omit either/bothTrueandFalse? The spec doesn't allow omission of any specific arguments per-say but it does allow omission of boolean indexing (technically a specified argument for get/set item), so maybe it's not too "magical" of a compromise.It could even be that a library which knows how it will return NaNs in
xp.unique()can allow the respectiveequal_nanbool arg to just work, and raise a helpful error for the opposite arg.jakirkham commentedon Sep 16, 2021
As an aside, we discovered this behavior on the call today.
jakirkham commentedon Sep 16, 2021
From a suggestion in the call, what if a
uniqueimplementation filtered outnans first? Something like this?pmeier commentedon Sep 16, 2021
@jakirkham
That is the same trap I fell into: Since
np.nanis only generated once, it is by design the identical object and thus will be filtered by the set:jakirkham commentedon Sep 16, 2021
Yeah was thinking that might be the case. Thanks for sharing Philip 🙂
honno commentedon Sep 19, 2021
A limitation with this example specifically is that boolean indexing might not be supported by an Array API library (it's optional behaviour in the spec, see #84). You could manually zip an array
awith its NaN maskxp.isnan(a)to then construct a unique array, but this could be painfully slow, and generally an awkward implementation (see Leo Fang's previous comment).honno commentedon Oct 11, 2021
In either the proposed
unique_all()(#275) or the currentunique(..., return_counts=True), I just wanted to clarify what counting behaviour we can expect from both NaNs-unique and NaNs-not-unique scenarios.I assume that we get the following if NaNs are unique to each-other:
And likewise if NaNs are not unique to each-other:
As you can see, the first scenario is a bit awkward in finding NaN counts as you'd still need to iterate over
valuesto make sure you hit everynan.Also to note, counting behaviour does not help us get a consistent count of unique values.
asmeurer commentedon Oct 11, 2021
I think we have to make counts and values consistent with one another, regardless of which nan approach we take. It would be nonsensical to return something like
values == array([nan, nan, nan, nan, nan]); counts = array([5]). Thenvaluesandcountswouldn't even correspond to each other any more.kgryte commentedon Oct 21, 2021
Comparison among environments:
MATLAB:
NaNs are distinct.Julia: returns only a single
NaNThe
uniqueimplementation is based onSet:And uses
isequalfor determining whether values are equal to one another.As an aside, Julia's
uniqueandSettreat0.0and-0.0as distinct.Python: depends on use of
set.Torch:
NaNs are distinct.TensorFlow:
NaNs are distinct.NumPy previously returned unique
NaNs. As ofv1.21, returns only a singleNaN.Downstream Libraries
np.uniquereturning multiple NaNs.np.uniqueto handle multiple NaNs. This wrapper assumes sorted unique values.Proposal
Unique should return multiple
NaNs (in-line with previous NumPy behavior and other libraries).Specify sort order for floating-point values (see gh-288)
To return only a single
NaN, users can implement a similar workaround to sklearn:There would be some additional work for getting the
indices,inverse_indices, andcounts.Another workaround for users is to select a sentinel value other than
NaNto indicate a "missing" value for the purposes of unique. For example, if values are known to be integers on the interval[1,100], replaceNaNvalues with0before callingunique.In short, the issue of multiple
NaNvalues seems, to me at least, to be a user concern and should thus be pushed to userland. While the utility of multipleNaNvalues in the output ofuniqueis undoubtedly limited, I don't see why the prevention of this should be the responsibility of array libraries, especially when bearing this responsibility entails a performance cost.