Classifique os itens em uma matriz usando Python / NumPy, sem classificar a matriz duas vezes

100

Eu tenho uma matriz de números e gostaria de criar outra matriz que representa a classificação de cada item na primeira matriz. Estou usando Python e NumPy.

Por exemplo:

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

Este é o melhor método que desenvolvi:

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

Há algum método melhor / mais rápido que evite classificar a matriz duas vezes?

joshayers
fonte
6
Sua última linha é equivalente a ranks = temp.argsort().
Sven Marnach

Respostas:

67

Use o fatiamento do lado esquerdo na última etapa:

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

Isso evita a classificação duas vezes, invertendo a permutação na última etapa.

Sven Marnach
fonte
3
Perfeito, obrigado! Eu sabia que havia uma solução e pareceria óbvio assim que a visse. Fiz alguns testes com o timeit, e esse método é um pouco mais lento para pequenos arrays. Na minha máquina, eles são iguais quando o array tem 2.000 elementos. Com 20.000 elementos, seu método é cerca de 25% mais rápido.
joshayers
alguma recomendação sobre como fazer isso rowwise?
Xaser
Para mais de 1 dim veja a resposta abaixo.
mathtick
100

Use argsort duas vezes, primeiro para obter a ordem da matriz e, em seguida, para obter a classificação:

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

Ao lidar com matrizes 2D (ou dimensões superiores), certifique-se de passar um argumento de eixo para argsort para ordenar sobre o eixo correto.

k.rooijers
fonte
2
Observe que se os números forem repetidos em sua matriz de entrada (por exemplo. [4,2,7,1,1]), A saída classificará esses números com base na posição da matriz ( [3,2,4,0,1])
rcoup
4
Classificar duas vezes é ineficiente. A resposta de @Sven Marnach mostra como realizar a classificação com um único telefonema para argsort.
Warren Weckesser
6
@WarrenWeckesser: Acabei de testar a diferença entre os dois, e você está certo para matrizes grandes, mas para qualquer coisa menor (n <100), argsort duplo é mais rápido (cerca de 20% mais rápido para n = 100 e cerca de 5 vezes mais rápido para n = 10). Portanto, se você precisar fazer muitas classificações em vários pequenos conjuntos de valores, esse método é muito melhor.
naught101 de
3
@WarrenWeckesser: Na verdade, estou errado, esse método é muito melhor. Ambos os métodos são muito mais rápidos do que o método scipy.stats também. Resultados: gist.github.com/naught101/14042d91a2d0f18a6ae4
naught101
1
@ naught101: Há um bug em seu script. A linha array = np.random.rand(10)deve ser array = np.random.rand(n).
Warren Weckesser
88

Essa pergunta tem alguns anos, e a resposta aceita é ótima, mas acho que o seguinte ainda vale a pena mencionar. Se você não se importa com a dependência scipy, pode usar 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])

Um bom recurso do rankdataé que o methodargumento fornece várias opções para lidar com empates. Por exemplo, existem três ocorrências de 20 e duas ocorrências de 40 em b:

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

O padrão atribui a classificação média aos valores empatados:

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

method='ordinal' atribui classificações consecutivas:

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

method='min' atribui a classificação mínima dos valores empatados a todos os valores empatados:

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

Veja a docstring para mais opções.

Warren Weckesser
fonte
1
sim, esta é a melhor resposta em qualquer lugar onde casos extremos são importantes.
naught101 de
Acho interessante que rankdataparece usar o mesmo mecanismo que a resposta aceita para gerar a classificação inicial internamente.
AlexV
5

Tentei estender ambas as soluções para matrizes A de mais de uma dimensão, supondo que você processe sua matriz linha por linha (eixo = 1).

Estendi o primeiro código com um loop em linhas; provavelmente pode ser melhorado

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

E o segundo, seguindo a sugestão de k.rooijers, torna-se:

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

Eu gerei aleatoriamente 400 matrizes com forma (1000,100); o primeiro código demorou cerca de 7,5, o segundo 3,8.

Igor Fobia
fonte
5

Para uma versão vetorizada de uma classificação média, veja abaixo. Eu amo o np.unique, ele realmente amplia o escopo do código que pode e não pode ser vetorizado com eficiência. Além de evitar python for-loops, essa abordagem também evita o duplo loop implícito sobre '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
fonte
a propósito; Fiz esse código para produzir a mesma saída que o outro código de classificação média, mas posso imaginar que a classificação mínima de um grupo de números repetidos funcione da mesma forma. Isso pode ser obtido ainda mais facilmente como >>> único, índice, inverso = np.unique (a, Verdadeiro, Verdadeiro) >>> rank_min = posto [índice] [inverso]
Eelco Hoogendoorn
Estou recebendo o seguinte erro com sua solução (numpy 1.7.1): AttributeError: 'numpy.ufunc' objeto não tem atributo 'at'
Medo
Isso requer uma versão mais recente do numpy; o seu é bastante antigo
Eelco Hoogendoorn
4

Além da elegância e da brevidade das soluções, há também a questão do desempenho. Aqui está uma pequena referência:

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
fonte
1
Boa ideia, mas para uma comparação justa, você deve usar rankdata(l, method='ordinal') - 1.
Warren Weckesser
3

Use argsort () duas vezes:

>>> array = [4,2,7,1]
>>> ranks = numpy.array(array).argsort().argsort()
>>> ranks
array([2, 1, 3, 0])
Kwong
fonte
2
isso foi mencionado bem antes de você
colocar
2

Tentei os métodos acima, mas falhei porque tinha muitos zeores. Sim, mesmo com flutuadores, itens duplicados podem ser importantes.

Então, escrevi uma solução 1D modificada adicionando uma etapa de verificação de empate:

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))

Acredito que seja o mais eficiente possível.

h2kyeong
fonte
0

Gostei do método de k.rooijers, mas, como escreveu rcoup, os números repetidos são classificados de acordo com a posição do array. Isso não foi bom para mim, então modifiquei a versão para pós-processar as classificações e mesclar quaisquer números repetidos em uma classificação média combinada:

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. ])

Espero que ajude outras pessoas também, tentei encontrar outra solução para isso, mas não consegui encontrar ...

Martin F Thomsen
fonte
0

argsort e slice são operações de simetria.

tente slice duas vezes em vez de argsort duas vezes. já que o slice é mais rápido do que argsort

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

Versão mais geral de uma das respostas:

In [140]: x = np.random.randn(10, 3)

In [141]: i = np.argsort(x, axis=0)

In [142]: ranks = np.empty_like(i)

In [143]: np.put_along_axis(ranks, i, np.repeat(np.arange(x.shape[0])[:,None], x.shape[1], axis=1), axis=0)

Consulte Como usar numpy.argsort () como índices em mais de 2 dimensões? para generalizar para mais escurecimentos.

matemática
fonte