NumPy - descending stable arg-sort of arrays of any dtype

1.5k Views Asked by At

NumPy's np.argsort is able to do stable sorting through passing kind = 'stable' argument.

Also np.argsort doesn't support reverse (descending) order.

If non-stable behavior is needed then descending order can be easily modeled through desc_ix = np.argsort(a)[::-1].

I'm looking for efficient/easy solution to descending-stable-sort NumPy's array a of any comparable dtype. See my meaning of "stability" in the last paragraph.

For the case when dtype is any numerical then stable descending arg-sorting can be easily done through sorting negated version of array:

print(np.argsort(-np.array([1, 2, 2, 3, 3, 3]), kind = 'stable'))
# prints: array([3, 4, 5, 1, 2, 0], dtype=int64)

But I need to support any comparable dtype including np.str_ and np.object_.

Just for clarification - maybe for descending orders classical meaning of stable means that equal elements are enumerated right to left. If so then in my question meaning of stable + descending is something different - equal ranges of elements should be enumerated left to right, while equal ranges between each other are ordered in descending order. I.e. same behavior should be achieved like in the last code above. I.e. I want stability in a sense same like Python achieves in next code:

print([e[0] for e in sorted(enumerate([1,2,2,3,3,3]), key = lambda e: e[1], reverse = True)])
# prints: [3, 4, 5, 1, 2, 0]
4

There are 4 best solutions below

3
On BEST ANSWER

I think this formula should work:

import numpy as np
a = np.array([1, 2, 2, 3, 3, 3])
s = len(a) - 1 - np.argsort(a[::-1], kind='stable')[::-1]
print(s)
# [3 4 5 1 2 0]
0
On

One simplest solution would be through mapping sorted unique elements of any dtype to ascending integers and then stable ascending arg-sorting of negated integers.

Try it online!

import numpy as np
a = np.array(['a', 'b', 'b', 'c', 'c', 'c'])
u = np.unique(a)
i = np.searchsorted(u, a)
desc_ix = np.argsort(-i, kind = 'stable')
print(desc_ix)
# prints [3 4 5 1 2 0]
1
On

We can make use of np.unique(..., return_inverse=True) -

u,tags = np.unique(a, return_inverse=True)
out = np.argsort(-tags, kind='stable')
0
On

Similar to @jdehesa's clean solution, this solution allows specifying an axis.

indices = np.flip(
    np.argsort(np.flip(x, axis=axis), axis=axis, kind="stable"), axis=axis
)
normalised_axis = axis if axis >= 0 else x.ndim + axis
max_i = x.shape[normalised_axis] - 1
indices = max_i - indices