Classez les éléments d'un tableau à l'aide de Python / NumPy, sans trier le tableau deux fois

100

J'ai un tableau de nombres et j'aimerais créer un autre tableau qui représente le rang de chaque élément dans le premier tableau. J'utilise Python et NumPy.

Par exemple:

array = [4,2,7,1]
ranks = [2,1,3,0]

Voici la meilleure méthode que j'ai trouvée:

array = numpy.array([4,2,7,1])
temp = array.argsort()
ranks = numpy.arange(len(array))[temp.argsort()]

Existe-t-il des méthodes meilleures / plus rapides qui évitent de trier le tableau deux fois?

Joshayers
la source
6
Votre dernière ligne équivaut à ranks = temp.argsort().
Sven Marnach

Réponses:

67

Utilisez le tranchage sur le côté gauche dans la dernière étape:

array = numpy.array([4,2,7,1])
temp = array.argsort()
ranks = numpy.empty_like(temp)
ranks[temp] = numpy.arange(len(array))

Cela évite de trier deux fois en inversant la permutation lors de la dernière étape.

Sven Marnach
la source
3
Parfait, merci! Je savais qu'il y avait une solution et cela semblerait évident une fois que je l'aurais vue. J'ai fait quelques tests avec timeit, et cette méthode est légèrement plus lente pour les petits tableaux. Sur ma machine, ils sont égaux lorsque le tableau contient 2000 éléments. À 20 000 éléments, votre méthode est environ 25% plus rapide.
joshayers
une recommandation sur la façon de faire cela en ligne?
Xaser
Pour plus d'un dim voir la réponse ci-dessous.
mathtick le
101

Utilisez argsort deux fois, d'abord pour obtenir l'ordre du tableau, puis pour obtenir le classement:

array = numpy.array([4,2,7,1])
order = array.argsort()
ranks = order.argsort()

Lorsque vous traitez avec des tableaux 2D (ou de dimension supérieure), assurez-vous de passer un argument d'axe à argsort pour passer le bon axe.

k.rooijers
la source
2
Notez que si des nombres sont répétés dans votre tableau d'entrée (par exemple [4,2,7,1,1]), la sortie classera ces nombres en fonction de leur position dans le tableau ( [3,2,4,0,1])
rcoup
4
Le tri deux fois est inefficace. La réponse de @Sven Marnach montre comment accomplir le classement en un seul appel argsort.
Warren Weckesser
6
@WarrenWeckesser: Je viens de tester la différence entre les deux, et vous avez raison pour les grands tableaux, mais pour tout ce qui est plus petit (n <100), le double tri est plus rapide (environ 20% plus rapide pour n = 100, et environ 5 fois plus rapide pour n = 10). Donc, si vous devez faire beaucoup de classements sur de nombreux petits ensembles de valeurs, cette méthode est bien meilleure.
rien101
3
@WarrenWeckesser: En fait, je me trompe, cette méthode est sans conteste meilleure. Les deux méthodes sont également beaucoup plus rapides que la méthode scipy.stats. Résultats: gist.github.com/naught101/14042d91a2d0f18a6ae4
naught101
1
@ naught101: Il y a un bogue dans votre script. La ligne array = np.random.rand(10)devrait être array = np.random.rand(n).
Warren Weckesser
88

Cette question date de quelques années et la réponse acceptée est excellente, mais je pense que ce qui suit mérite d'être mentionné. Si cela ne vous dérange pas de la dépendance scipy, vous pouvez utiliser scipy.stats.rankdata:

In [22]: from scipy.stats import rankdata

In [23]: a = [4, 2, 7, 1]

In [24]: rankdata(a)
Out[24]: array([ 3.,  2.,  4.,  1.])

In [25]: (rankdata(a) - 1).astype(int)
Out[25]: array([2, 1, 3, 0])

Une fonctionnalité intéressante de rankdataest que l' methodargument offre plusieurs options pour gérer les liens. Par exemple, il y a trois occurrences de 20 et deux occurrences de 40 dans b:

In [26]: b = [40, 20, 70, 10, 20, 50, 30, 40, 20]

La valeur par défaut attribue le rang moyen aux valeurs liées:

In [27]: rankdata(b)
Out[27]: array([ 6.5,  3. ,  9. ,  1. ,  3. ,  8. ,  5. ,  6.5,  3. ])

method='ordinal' attribue des rangs consécutifs:

In [28]: rankdata(b, method='ordinal')
Out[28]: array([6, 2, 9, 1, 3, 8, 5, 7, 4])

method='min' affecte le rang minimum des valeurs liées à toutes les valeurs liées:

In [29]: rankdata(b, method='min')
Out[29]: array([6, 2, 9, 1, 2, 8, 5, 6, 2])

Voir la docstring pour plus d'options.

Warren Weckesser
la source
1
oui, c'est la meilleure réponse partout où les cas de bord sont importants.
naught101 du
Je trouve intéressant que rankdatasemble utiliser le même mécanisme que la réponse acceptée pour générer le classement initial en interne.
AlexV
5

J'ai essayé d'étendre les deux solutions pour les tableaux A de plus d'une dimension, en supposant que vous traitez votre tableau ligne par ligne (axe = 1).

J'ai étendu le premier code avec une boucle sur les lignes; il peut probablement être amélioré

temp = A.argsort(axis=1)
rank = np.empty_like(temp)
rangeA = np.arange(temp.shape[1])
for iRow in xrange(temp.shape[0]): 
    rank[iRow, temp[iRow,:]] = rangeA

Et le second, suivant la suggestion de k.rooijers, devient:

temp = A.argsort(axis=1)
rank = temp.argsort(axis=1)

J'ai généré au hasard 400 tableaux de forme (1000, 100); le premier code prenait environ 7,5, le second 3,8.

Igor Fobia
la source
5

Pour une version vectorisée d'un rang moyen, voir ci-dessous. J'adore np.unique, cela élargit vraiment la portée de ce que le code peut et ne peut pas être efficacement vectorisé. En plus d'éviter les boucles for python, cette approche évite également la double boucle implicite sur «a».

import numpy as np

a = np.array( [4,1,6,8,4,1,6])

a = np.array([4,2,7,2,1])
rank = a.argsort().argsort()

unique, inverse = np.unique(a, return_inverse = True)

unique_rank_sum = np.zeros_like(unique)
np.add.at(unique_rank_sum, inverse, rank)
unique_count = np.zeros_like(unique)
np.add.at(unique_count, inverse, 1)

unique_rank_mean = unique_rank_sum.astype(np.float) / unique_count

rank_mean = unique_rank_mean[inverse]

print rank_mean
Eelco Hoogendoorn
la source
au fait; J'ai créé ce code pour produire le même résultat que l'autre code de classement moyen, mais je peux imaginer que le rang minimum d'un groupe de nombres répétitifs fonctionne aussi bien. Ceci peut être obtenu encore plus facilement comme >>> unique, index, inverse = np.unique (a, True, True) >>> rank_min = rank [index] [inverse]
Eelco Hoogendoorn
J'obtiens l'erreur suivante avec votre solution (numpy 1.7.1): AttributeError: l'objet 'numpy.ufunc' n'a pas d'attribut 'at'
Peur
Cela nécessite une version plus récente de numpy; le vôtre est assez ancien
Eelco Hoogendoorn
4

Outre l'élégance et la brièveté des solutions, il y a aussi la question de la performance. Voici un petit repère:

import numpy as np
from scipy.stats import rankdata
l = list(reversed(range(1000)))

%%timeit -n10000 -r5
x = (rankdata(l) - 1).astype(int)
>>> 128 µs ± 2.72 µs per loop (mean ± std. dev. of 5 runs, 10000 loops each)

%%timeit -n10000 -r5
a = np.array(l)
r = a.argsort().argsort()
>>> 69.1 µs ± 464 ns per loop (mean ± std. dev. of 5 runs, 10000 loops each)

%%timeit -n10000 -r5
a = np.array(l)
temp = a.argsort()
r = np.empty_like(temp)
r[temp] = np.arange(len(a))
>>> 63.7 µs ± 1.27 µs per loop (mean ± std. dev. of 5 runs, 10000 loops each)
Mischa Lisovyi
la source
1
Bonne idée, mais pour une comparaison équitable, vous devriez utiliser rankdata(l, method='ordinal') - 1.
Warren Weckesser le
3

Utilisez argsort () deux fois pour le faire:

>>> array = [4,2,7,1]
>>> ranks = numpy.array(array).argsort().argsort()
>>> ranks
array([2, 1, 3, 0])
Kwong
la source
2
cela a déjà été mentionné bien avant que vous ne
posiez
2

J'ai essayé les méthodes ci-dessus, mais j'ai échoué car j'avais beaucoup de zeores. Oui, même avec des éléments flottants, les doublons peuvent être importants.

J'ai donc écrit une solution 1D modifiée en ajoutant une étape de vérification des liens:

def ranks (v):
    import numpy as np
    t = np.argsort(v)
    r = np.empty(len(v),int)
    r[t] = np.arange(len(v))
    for i in xrange(1, len(r)):
        if v[t[i]] <= v[t[i-1]]: r[t[i]] = r[t[i-1]]
    return r

# test it
print sorted(zip(ranks(v), v))

Je pense que c'est aussi efficace que possible.

h2kyeong
la source
0

J'ai aimé la méthode de k.rooijers, mais comme l'a écrit rcoup, les nombres répétés sont classés en fonction de la position du tableau. Ce n'était pas bon pour moi, j'ai donc modifié la version pour post-traiter les rangs et fusionner tous les nombres répétés en un rang moyen combiné:

import numpy as np
a = np.array([4,2,7,2,1])
r = np.array(a.argsort().argsort(), dtype=float)
f = a==a
for i in xrange(len(a)):
   if not f[i]: continue
   s = a == a[i]
   ls = np.sum(s)
   if ls > 1:
      tr = np.sum(r[s])
      r[s] = float(tr)/ls
   f[s] = False

print r  # array([ 3. ,  1.5,  4. ,  1.5,  0. ])

J'espère que cela pourrait aussi aider les autres, j'ai essayé de trouver une autre solution à cela, mais je n'ai pas trouvé de solution ...

Martin F Thomsen
la source
0

argsort et slice sont des opérations de symétrie.

essayez slice deux fois au lieu d'argsort deux fois. puisque slice est plus rapide que argsort

array = numpy.array([4,2,7,1])
order = array.argsort()
ranks = np.arange(array.shape[0])[order][order]
yupbank
la source