17

I want to get the rank of each element, so I use argsort in numpy:

np.argsort(np.array((1,1,1,2,2,3,3,3,3)))
array([0, 1, 2, 3, 4, 5, 6, 7, 8])

it give the same element the different rank, can I get the same rank like:

array([0, 0, 0, 3, 3, 5, 5, 5, 5])
Psidom
  • 209,562
  • 33
  • 339
  • 356
maple
  • 1,828
  • 2
  • 19
  • 28

5 Answers5

18

If you don't mind a dependency on scipy, you can use scipy.stats.rankdata, with method='min':

In [14]: a
Out[14]: array([1, 1, 1, 2, 2, 3, 3, 3, 3])

In [15]: from scipy.stats import rankdata

In [16]: rankdata(a, method='min')
Out[16]: array([1, 1, 1, 4, 4, 6, 6, 6, 6])

Note that rankdata starts the ranks at 1. To start at 0, subtract 1 from the result:

In [17]: rankdata(a, method='min') - 1
Out[17]: array([0, 0, 0, 3, 3, 5, 5, 5, 5])

If you don't want the scipy dependency, you can use numpy.unique to compute the ranking. Here's a function that computes the same result as rankdata(x, method='min') - 1:

import numpy as np

def rankmin(x):
    u, inv, counts = np.unique(x, return_inverse=True, return_counts=True)
    csum = np.zeros_like(counts)
    csum[1:] = counts[:-1].cumsum()
    return csum[inv]

For example,

In [137]: x = np.array([60, 10, 0, 30, 20, 40, 50])

In [138]: rankdata(x, method='min') - 1
Out[138]: array([6, 1, 0, 3, 2, 4, 5])

In [139]: rankmin(x)
Out[139]: array([6, 1, 0, 3, 2, 4, 5])

In [140]: a = np.array([1,1,1,2,2,3,3,3,3])

In [141]: rankdata(a, method='min') - 1
Out[141]: array([0, 0, 0, 3, 3, 5, 5, 5, 5])

In [142]: rankmin(a)
Out[142]: array([0, 0, 0, 3, 3, 5, 5, 5, 5])

By the way, a single call to argsort() does not give ranks. You can find an assortment of approaches to ranking in the question Rank items in an array using Python/NumPy, including how to do it using argsort().

Community
  • 1
  • 1
Warren Weckesser
  • 110,654
  • 19
  • 194
  • 214
8

Alternatively, pandas series has a rank method which does what you need with the min method:

import pandas as pd
pd.Series((1,1,1,2,2,3,3,3,3)).rank(method="min")

# 0    1
# 1    1
# 2    1
# 3    4
# 4    4
# 5    6
# 6    6
# 7    6
# 8    6
# dtype: float64
Warren Weckesser
  • 110,654
  • 19
  • 194
  • 214
Psidom
  • 209,562
  • 33
  • 339
  • 356
  • Was trying to incorporate this for timings, but threw this error : `ValueError: No axis named min for object type `. Do you happen to know what's going on there? I am on `0.18.1` pandas version and `python 2.7`. – Divakar Aug 21 '16 at 04:57
  • @Divakar, the argument to `rank()` must be `method="min"`. – Warren Weckesser Aug 21 '16 at 13:01
3

With focus on performance, here's an approach -

def rank_repeat_based(arr):
    idx = np.concatenate(([0],np.flatnonzero(np.diff(arr))+1,[arr.size]))
    return np.repeat(idx[:-1],np.diff(idx))

For a generic case with the elements in input array not already sorted, we would need to use argsort() to keep track of the positions. So, we would have a modified version, like so -

def rank_repeat_based_generic(arr):    
    sidx = np.argsort(arr,kind='mergesort')
    idx = np.concatenate(([0],np.flatnonzero(np.diff(arr[sidx]))+1,[arr.size]))
    return np.repeat(idx[:-1],np.diff(idx))[sidx.argsort()]

Runtime test

Testing out all the approaches listed thus far to solve the problem on a large dataset.

Sorted array case :

In [96]: arr = np.sort(np.random.randint(1,100,(10000)))

In [97]: %timeit rankdata(arr, method='min') - 1
1000 loops, best of 3: 635 µs per loop

In [98]: %timeit rankmin(arr)
1000 loops, best of 3: 495 µs per loop

In [99]: %timeit (pd.Series(arr).rank(method="min")-1).values
1000 loops, best of 3: 826 µs per loop

In [100]: %timeit rank_repeat_based(arr)
10000 loops, best of 3: 200 µs per loop

Unsorted case :

In [106]: arr = np.random.randint(1,100,(10000))

In [107]: %timeit rankdata(arr, method='min') - 1
1000 loops, best of 3: 963 µs per loop

In [108]: %timeit rankmin(arr)
1000 loops, best of 3: 869 µs per loop

In [109]: %timeit (pd.Series(arr).rank(method="min")-1).values
1000 loops, best of 3: 1.17 ms per loop

In [110]: %timeit rank_repeat_based_generic(arr)
1000 loops, best of 3: 1.76 ms per loop
Divakar
  • 218,885
  • 19
  • 262
  • 358
  • This requires that the input is already sorted. That might be fine, because the only example provided in the question is, in fact, already sorted. @maple, is that the case? Will the input always be sorted? – Warren Weckesser Aug 21 '16 at 12:49
  • @WarrenWeckesser Added solution for a generic case. – Divakar Aug 21 '16 at 13:18
0

I've written a function for the same purpose. It uses pure python and numpy only. Please have a look. I put comments as well.

def my_argsort(array):
    # this type conversion let us work with python lists and pandas series
    array = np.array(array)
    # create mapping for unique values
    # it's a dictionary where keys are values from the array and
    # values are desired indices 
    unique_values = list(set(array))
    mapping = dict(zip(unique_values, np.argsort(unique_values)))
    # apply mapping to our array
    # np.vectorize works similar map(), and can work with dictionaries
    array = np.vectorize(mapping.get)(array)
    return array

Hope that helps.

verymax
  • 1
  • 1
0

Complex solutions are unnecessary for this problem.

> ary = np.sort([1, 1, 1, 2, 2, 3, 3, 3, 3])  # or anything; must be sorted.
> a = np.diff().cumsum(); a
array([0, 0, 1, 1, 2, 2, 2, 2])
> b = np.r_[0, a]; b  # ties get first open rank
array([0, 0, 0, 1, 1, 2, 2, 2, 2]) 
> c = np.flatnonzero(ary[1:] != ary[:-1])
> np.r_[0, 1 + c][b]  # ties get last open rank
array([0, 0, 0, 3, 3, 5, 5, 5])
nth
  • 1,442
  • 15
  • 12