Repository: numpy/numpy

Issue:
BUG: np.unique(a, axis=0) sorted incorrectly when values cross 255
This is a weird one I noticed while calling `unique(a, axis=0)` for a 2D array `a`. If some values in the first column of `a` are `<= 255` and others are `>255`, the output is incorrectly sorted. Maybe something somewhere is making an assumption about single bytes and causing an overflow.
```python
>>> # broken:
... a = np.array([[255, 256],
...               [256, 257]])
>>> np.unique(a, axis=0)
array([[256, 257],
       [255, 256]])
>>> # row order doesn't matter, also broken:
... a = np.array([[256, 257],
...               [255, 256]])
>>> np.unique(a, axis=0)
array([[256, 257],
       [255, 256]])
>>> # works:
... a = np.array([[254, 255],
...               [255, 256]])
>>> np.unique(a, axis=0)
array([[254, 255],
       [255, 256]])
>>> # works:
... a = np.array([[256, 257],
...               [257, 258]])
>>> np.unique(a, axis=0)
array([[256, 257],
       [257, 258]])
>>> a.dtype
dtype('int64')
>>> a.flags
  C_CONTIGUOUS : True
  F_CONTIGUOUS : False
  OWNDATA : True
  WRITEABLE : True
  ALIGNED : True
  WRITEBACKIFCOPY : False
  UPDATEIFCOPY : False
```
I just noticed this also happens for 1D arrays when the axis kwarg is used:
```python
>>> a = np.array([255, 256])
>>> np.unique(a, axis=0)
array([256, 255])
>>> np.unique(a)
array([255, 256])
```
It seems the `axis` kwarg is a relatively new feature for `np.unique`. This is in Xubuntu 16.04 amd64.
```
Python 3.5.2 (default, Nov 23 2017, 16:37:01) 
[GCC 5.4.0 20160609] on linux
>>> np.version.version
'1.14.0'
```



Discussion:
Looks like an endianess issue. What does `np.array(257).view((np.uint8, 8))` give you?
```python
>>> np.array(257).view((np.uint8, 8))
array([1, 1, 0, 0, 0, 0, 0, 0], dtype=uint8)
```
This is a 6th gen i7 laptop.
Turns out this optimization is not valid:

https://github.com/numpy/numpy/blob/365fc9d19b52325bd5618aac1a3fa7b8a6be3b3c/numpy/lib/arraysetops.py#L220-L225

If we want to preserve sort order, we need this to be little-endian too.

In practice, I think we'd do better to remove the guarantee that the result is sorted.
Hmm, the `axis` argument is new in 1.13, so presumably no older code is broken. Sort order needs to be defined in the documentation for the `axis=<int>` case. We could use a lexigraphical order on the integer values -- would want to reorder the memory layout in that case -- or we could just say is it in lexigraphical order of the "rows" considered as byte strings. If the uniqueness and mapping are the important parts of the functionality, I'd be inclined to leave it as is. I do think that when the current byte strings are long, it might be faster to always argsort and then reorder, swapping lengthy rows is not an efficient operation.
It's not clear to me that the optimization is particularly worthwhile, especially if there are large numbers of rows. Are structured arrays really that slow?
> Are structured arrays really that slow?

One would need to profile to see what the tradeoff is. But the "elements" of the reshaped array are flattened subarrays of the original, so one is moving whole subarrays around, which has got to be slower than just moving `npy_intp` around. Of course, one still has to copy over the results into a new array at the end, but that is just O(n) as opposed to O(n log(n)) for the sort itself.  If one didn't need to return the sorted array, it should be possible to just work with the indexes.
I think there's a PR that tries to make unique more efficient, and does pretty much just that - but it suffered from scope creep, and started doing questionable things like crawling the `.base` graph to avoid a copy.

edit: #9531
> But the "elements" of the reshaped array are flattened subarrays of the original, so one is moving whole subarrays around, which has got to be slower than just moving npy_intp around.

I'm not sure I understand the relevance here - the optimization is making a tradeoff between viewing as a structured array with a field per column and viewing as `bytes`. The sort is inefficient in both cases, as the same amount of memory is copied - what makes a difference is whether the comparison/copy function is faster for raw `void` than for structured `void`.
Just mentioning @martinosorb who added the `axes` kwarg.
There are two types of sorts used. If the indexes are needed, the array is argsorted, if not, then just sorted. In the first case, indexes are moved, in the second, subarrays. Not just moved, but swapped, three copy operations in addition to the compare. I'd guess at least 2x as much time is taken to do that when we are talking large elements.  
Sure, but AFAICT the optimization I link to above is not choosing between those two types of sorts - so what you're describing is orthogonal to the optimization that causes this bug/undocumented behavior.
Yes, it is orthogonal, nevertheless, it is a possible optimization. Currently
```
    if optional_indices:
        perm = ar.argsort(kind='mergesort' if return_index else 'quicksort')
        aux = ar[perm]
    else:
        ar.sort()
        aux = ar
```
OTOH, I don't know what the difference in comparison speeds is, IIRC the comparison function for structured arrays is constructed on the fly depending on the `order` passed.
@mspacek Thanks for calling me. An odd bug really! I mostly revived an old pull request, those lines were written by @joferkington, who may have some additional insight.
Just to chime in:

The `np.void` optimization isn't huge, but I recall it being significant (~2x) in some cases.  That's worth re-profiling -- that was a long time ago, and I may be completely mistaken.  

Some quick tests indicate that it's slower on my system than using a struct array for simple cases (e.g. uniform data). It very well may not be worth the complexity it adds, particularly if it's introducing endianness bugs.
@charris:

> I do think that when the current byte strings are long, it might be faster to always argsort and then reorder, swapping lengthy rows is not an efficient operation.

To be clear, your claim is that we should replace
```
a.sort()
```
with
```
i = a.argsort()
a = a[i]
```

because the number of copies that occur should reduce (to at most one swap per element)?

I'd buy that, although maybe that optimization should be made within `sort` itself?

Is there a way to implement `a = a[i]` without doubling the memory usage? Perhaps we need to do that in C, as a `(i),(i)->(i)` ufunc
It is complicated by cache effects in practice. Both quicksort and mergesort have sequential memory access, which is cache friendly, whereas argsorts tend more towards random memory access to get at the elements to be compared, so it is a tradeoff and depends on element size and other things. The reason that lexsort is indirect is because it was assumed that it would be used to sort large structures on small keys.

It is possible to do `a = a[i]` inplace using the factorization of the permutation into cycles. Simple sorting does that also, but less efficiently. I don't know how much all that complication buys you. 
I can confirm that removing the optimization...

https://github.com/numpy/numpy/blob/365fc9d19b52325bd5618aac1a3fa7b8a6be3b3c/numpy/lib/arraysetops.py#L220-L225

...fixes the issue. Shall I go ahead and make a PR with this change, and maybe add a test for it?

@mspacek  I don't think we have much choice if we want to preserve the expected sort order. I would have preferred that uniqueness be orthogonal to sorting, but here we are. We could, I suppose, leave it as is and document the unexpected sort order for the axis sorts, but unless it is an unqualified win it probably isn't worth the hassle. A couple of benchmarks might help with the final decision.
Looks like it's ~2X faster with the optimization, from a couple of fairly naive tests on my late model i7 ultrabook:
```python
a = np.random.randint(0, 2**16, 1000000)
# optimized, 1D:
timeit np.unique(a, axis=0)
# 272 ms ± 1.55 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
# non-optimized, 1D:
timeit np.unique(a, axis=0)
# 541 ms ± 7.77 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
a.shape = -1, 10
# optimized, 2D:
timeit np.unique(a, axis=0)
# 32.2 ms ± 276 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
# non-optimized, 2D:
timeit np.unique(a, axis=0)
# 66.4 ms ± 232 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
```
Performance is unsurprisingly identical when the `axis` kwarg isn't used.
```python
a = np.random.randint(0, 2**16, 1000000)
# optimized:
timeit np.unique(a)
# 68.7 ms ± 631 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
# non-optimized:
timeit np.unique(a)
# 68 ms ± 136 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
```
Note that with the optimization, if I force an in-place sort along the appropriate axis before returning (to get the correct result), I get almost the same performance in the 1D case, but worse performance than even without the optimization in the 2D case:
```python
# optimized with in-place sort, output is now correct:
np.unique([255, 256], axis=0)
array([255, 256])
# 1D:
a = np.random.randint(0, 2**16, 1000000)
timeit np.unique(a, axis=0)
# 281 ms ± 4.42 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
# 2D:
a.shape = -1, 10
timeit np.unique(a, axis=0)
# 98.5 ms ± 439 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
```
The last case is pretty much what I'm currently doing in my own code to get the correct result, so worst of both worlds!

Also, it seems problematic to me that specifying `axis=0` in the 1D case should result in such a big slowdown, both with and without the optimization.
I've put up #10568 that simply documents the possible sort orders for discussion. That said, I think that the original intent of the optimization was that it would sort in the same way as the structured case and that the failure for little endian (and signed, unsigned) was an oversight, else why limit it to integer types?
I wonder whether the issue here is that:

    dtype = [('f{i}'.format(i=i), ar.dtype) for i in range(ar.shape[1])]

Is slow to sort, and instead of adding the current optimization to help with that, we should just do

    dtype = [('f', (ar.dtype, ar.shape[1]))]

Perhaps subarray sorts are faster?
Subarray sorts are broken in exactly the same way as this optimization: #10570
@eric-wieser The problem of sorting arrays of structured dtypes has a history going way back, and sorting them like voids was, IIRC, the original solution. For the current problem, I'm inclining to removing the optimization for 1.14 and making it depend on a new keyword, maybe "inplace". If not "inplace=True", we can do as we please with the array copy internally, the only thing that matters is identifying the unique elements.
BTW, for structured aligned dtypes, any padding had better have a fixed value, say zero.
Its conceivable that in the future we will get a fast way to sort rows, so I don't think we should set bad behavior in stone now. For that reason I'm not even sure we should add a keyword. My vote is either to remove the optimization in order to get the "common sense" output, or to leave the optimization but clearly say in the docstring that we no longer guarantee sorting when axis is used (like  #10568).
I vote for removing the optimization for now, but also ensuring that setting `axis=0` for a 1D array doesn't cause a slowdown relative to the default `axis=None`. Sorted output is a strong expectation for me, and making exceptions based on kwarg values seems messy.
Optimization removed in #10588.

Resolving patch:
diff --git a/numpy/lib/arraysetops.py b/numpy/lib/arraysetops.py
--- a/numpy/lib/arraysetops.py
+++ b/numpy/lib/arraysetops.py
@@ -135,16 +135,18 @@ def unique(ar, return_index=False, return_inverse=False,
     return_counts : bool, optional
         If True, also return the number of times each unique item appears
         in `ar`.
+
         .. versionadded:: 1.9.0
-    axis : int or None, optional
-        The axis to operate on. If None, `ar` will be flattened beforehand.
-        Otherwise, duplicate items will be removed along the provided axis,
-        with all the other axes belonging to the each of the unique elements.
-        Object arrays or structured arrays that contain objects are not
-        supported if the `axis` kwarg is used.
-        .. versionadded:: 1.13.0
 
+    axis : int or None, optional
+        The axis to operate on. If None, `ar` will be flattened. If an integer,
+        the subarrays indexed by the given axis will be flattened and treated
+        as the elements of a 1-D array with the dimension of the given axis,
+        see the notes for more details.  Object arrays or structured arrays
+        that contain objects are not supported if the `axis` kwarg is used. The
+        default is None.
 
+        .. versionadded:: 1.13.0
 
     Returns
     -------
@@ -166,6 +168,17 @@ def unique(ar, return_index=False, return_inverse=False,
     numpy.lib.arraysetops : Module with a number of other functions for
                             performing set operations on arrays.
 
+    Notes
+    -----
+    When an axis is specified the subarrays indexed by the axis are sorted.
+    This is done by making the specified axis the first dimension of the array
+    and then flattening the subarrays in C order. The flattened subarrays are
+    then viewed as a structured type with each element given a label, with the
+    effect that we end up with a 1-D array of structured types that can be
+    treated in the same way as any other 1-D array. The result is that the
+    flattened subarrays are sorted in lexicographic order starting with the
+    first element.
+
     Examples
     --------
     >>> np.unique([1, 1, 2, 2, 3, 3])
@@ -217,14 +230,7 @@ def unique(ar, return_index=False, return_inverse=False,
     ar = ar.reshape(orig_shape[0], -1)
     ar = np.ascontiguousarray(ar)
 
-    if ar.dtype.char in (np.typecodes['AllInteger'] +
-                         np.typecodes['Datetime'] + 'S'):
-        # Optimization: Creating a view of your data with a np.void data type of
-        # size the number of bytes in a full row. Handles any type where items
-        # have a unique binary representation, i.e. 0 is only 0, not +0 and -0.
-        dtype = np.dtype((np.void, ar.dtype.itemsize * ar.shape[1]))
-    else:
-        dtype = [('f{i}'.format(i=i), ar.dtype) for i in range(ar.shape[1])]
+    dtype = [('f{i}'.format(i=i), ar.dtype) for i in range(ar.shape[1])]
 
     try:
         consolidated = ar.view(dtype)


Test patch:
diff --git a/numpy/lib/tests/test_arraysetops.py b/numpy/lib/tests/test_arraysetops.py
--- a/numpy/lib/tests/test_arraysetops.py
+++ b/numpy/lib/tests/test_arraysetops.py
@@ -453,6 +453,15 @@ def test_unique_masked(self):
         assert_array_equal(v.data, v2.data, msg)
         assert_array_equal(v.mask, v2.mask, msg)
 
+    def test_unique_sort_order_with_axis(self):
+        # These tests fail if sorting along axis is done by treating subarrays
+        # as unsigned byte strings.  See gh-10495.
+        fmt = "sort order incorrect for integer type '%s'"
+        for dt in 'bhilq':
+            a = np.array([[-1],[0]], dt)
+            b = np.unique(a, axis=0)
+            assert_array_equal(a, b, fmt % dt)
+
     def _run_axis_tests(self, dtype):
         data = np.array([[0, 1, 0, 0],
                          [1, 0, 0, 0],