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?


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.


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.


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.


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.


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

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)

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

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.


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


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]

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.