4

There is a 2D numpy array of about 500000 rows by 512 values each row:

[
  [1,0,1,...,0,0,1], # 512 1's or 0's
  [0,1,0,...,0,1,1],
  ...
  [0,0,1,...,1,0,1], # row number 500000
]

How to sort the rows ascending as if each row is a long 512-bit integer?

[
  [0,0,1,...,1,0,1],
  [0,1,0,...,0,1,1],
  [1,0,1,...,0,0,1],
  ...
]
Serge
  • 1,531
  • 2
  • 21
  • 44
  • I'm not sure but the function "sorted" works to sort strings which are arrays of characters, maybe it works with integer arrays ? – Orionss Oct 26 '17 at 11:17
  • @Orionss Bad idea. Don't bring python functions where numpy arrays are involved. – cs95 Oct 26 '17 at 11:18

4 Answers4

4

Instead of converting to strings you can also use a void view (as from @Jaime here) of the data and argsort by that.

def sort_bin(b):
    b_view = np.ascontiguousarray(b).view(np.dtype((np.void, b.dtype.itemsize * b.shape[1])))
    return b[np.argsort(b_view.ravel())] #as per Divakar's suggestion

Testing

np.random.seed(0)

b = np.random.randint(0, 2, (10,5))
print(b)
print(sort_bin(b))

[[0 1 1 0 1]
 [1 1 1 1 1]
 [1 0 0 1 0]
 ..., 
 [1 0 1 1 0]
 [0 1 0 1 1]
 [1 1 1 0 1]]
[[0 0 0 0 1]
 [0 1 0 1 1]
 [0 1 1 0 0]
 ..., 
 [1 1 1 0 1]
 [1 1 1 1 0]
 [1 1 1 1 1]]

Should be much faster and less memory-intensive since b_view is just a view into b

t = np.random.randint(0,2,(2000,512))

%timeit sort_bin(t)
100 loops, best of 3: 3.09 ms per loop

%timeit np.array([[int(i) for i in r] for r in np.sort(np.apply_along_axis(lambda r: ''.join([str(c) for c in r]), 0, t))])
1 loop, best of 3: 3.29 s per loop

About 1000x faster actually

Daniel F
  • 13,620
  • 2
  • 29
  • 55
0

You could sort them in a stable way 512 times, starting with the right-most bit first.

  1. Sort by last bit
  2. Sort by second-last bit, stable (to not mess up results of previous sort)
  3. ... ...
  4. Sort by first bit, stable

A smaller example: assume you want to sort these three 2-bit numbers by bits:

11
01
00

In the first step, you sort by the right bit, resulting in:

00
11
01

Now you sort by the first bit, in this case we have two 0s in that column. If your sorting algorithm is not stable it would be allowed to put these equal items in any order in the result, that could cause 01 to appear before 00 which we do not want, so we use a stable sort, keeping the relative order of equal items, for the first column, resulting in the desired:

00
01
11
gonutz
  • 5,087
  • 3
  • 22
  • 40
  • You could probably use `np.lexsort` for this, but I'm not sure it would be faster than a string/void conversion. – Daniel F Oct 26 '17 at 12:00
0

Creating a string of each row and then applying np.sort()

So if we have an array to test on:

a = np.array([[1,0,0,0],[0,0,0,0],[1,1,1,1],[0,0,1,1]])

We can create strings of each row by using np.apply_along_axis:

a = np.apply_along_axis(lambda r: ''.join([str(c) for c in r]), 0, a)

which would make a now:

array(['1010', '0010', '0011', '0011'], dtype='<U4')

and so now we can sort the strings with np.sort():

a = np.sort(a)

making a:

array(['0010', '0011', '0011', '1010'], dtype='<U4')

we can then convert back to the original format with:

a = np.array([[int(i) for i in r] for r in a])

which makes a:

array([[0, 0, 1, 0],
       [0, 0, 1, 1],
       [0, 0, 1, 1],
       [1, 0, 1, 0]])

And if you wanted to cram this all into one line:

a = np.array([[int(i) for i in r] for r in np.sort(np.apply_along_axis(lambda r: ''.join([str(c) for c in r]), 0, a))])
Joe Iddon
  • 20,101
  • 7
  • 33
  • 54
  • Thanks for the suggestion. It just seems "wrong" to convert single bits to character bytes. Maybe [numpy.packbits()](https://docs.scipy.org/doc/numpy-1.13.0/reference/generated/numpy.packbits.html#numpy.packbits) each row to 64 uint8's and then use your method? – Serge Oct 26 '17 at 11:57
0

This is slow but does the job.

def sort_col(arr, col_num=0):
# if we have sorted over all columns return array
if col_num >= arr.shape[1]:
    return arr

# sort array over given column
arr_sorted = arr[arr[:, col_num].argsort()]

# if the number of 1s in the given column is not equal to the total number
# of rows neither equal to 0, split on 1 and 0, sort and then merge
if len(arr) > np.sum(arr_sorted[:, col_num]) > 0:
    arr_sorted0s = sort_col(arr_sorted[arr_sorted[:, col_num]==0], col_num+1)
    arr_sorted1s = sort_col(arr_sorted[arr_sorted[:, col_num]==1], col_num+1)
    # change order of stacking if you want ascenting order
    return np.vstack((arr_sorted0s, arr_sorted1s))

# if the number of 1s in the given column is equal to the total number
# of rows or equal to 0, just go to the next iteration
return sort_col(arr_sorted, col_num + 1)



np.random.seed(0)
a = np.random.randint(0, 2, (5, 4))
print(a)
print(sort_col(a))

# prints
[[0 1 1 0]
 [1 1 1 1]
 [1 1 1 0]
 [0 1 0 0]
 [0 0 0 1]]
[[0 0 0 1]
 [0 1 0 0]
 [0 1 1 0]
 [1 1 1 0]
 [1 1 1 1]]

Edit. Or better yet use Daniels solution. I didn't check for new answers before I posted my code.