web-dev-qa-db-fra.com

Mappe chaque valeur de liste au centile correspondant

J'aimerais créer une fonction qui prend une liste (triée) comme argument et génère une liste contenant le centile correspondant à chaque élément.

Par exemple, fn([1,2,3,4,17]) renvoie [0.0, 0.25, 0.50, 0.75, 1.00].

Quelqu'un peut-il s'il vous plaît soit:

  1. Aidez-moi à corriger mon code ci-dessous? OU
  2. Offrir une meilleure alternative que mon code pour mapper les valeurs d'une liste sur leurs centiles correspondants?

Mon code actuel:

def median(mylist):
    length = len(mylist)
    if not length % 2:
        return (mylist[length / 2] + mylist[length / 2 - 1]) / 2.0
    return mylist[length / 2]

###############################################################################
# PERCENTILE FUNCTION
###############################################################################

def percentile(x):
    """
    Find the correspoding percentile of each value relative to a list of values.
    where x is the list of values
    Input list should already be sorted!
    """

    # sort the input list
    # list_sorted = x.sort()

    # count the number of elements in the list
    list_elementCount = len(x)

    #obtain set of values from list

    listFromSetFromList = list(set(x))

    # count the number of unique elements in the list
    list_uniqueElementCount = len(set(x))

    # define extreme quantiles
    percentileZero    = min(x)
    percentileHundred = max(x)

    # define median quantile
    mdn = median(x) 

    # create empty list to hold percentiles
    x_percentile = [0.00] * list_elementCount 

    # initialize unique count
    uCount = 0

    for i in range(list_elementCount):
        if x[i] == percentileZero:
            x_percentile[i] = 0.00
        Elif x[i] == percentileHundred:
            x_percentile[i] = 1.00
        Elif x[i] == mdn:
            x_percentile[i] = 0.50 
        else:
            subList_elementCount = 0
            for j in range(i):
                if x[j] < x[i]:
                    subList_elementCount = subList_elementCount + 1 
            x_percentile[i] = float(subList_elementCount / list_elementCount)
            #x_percentile[i] = float(len(x[x > listFromSetFromList[uCount]]) / list_elementCount)
            if i == 0:
                continue
            else:
                if x[i] == x[i-1]:
                    continue
                else:
                    uCount = uCount + 1
    return x_percentile

Actuellement, si je soumets percentile([1,2,3,4,17]), la liste [0.0, 0.0, 0.5, 0.0, 1.0] est renvoyée.

21
Jubbles

Je pense que votre exemple d’entrée/sortie ne correspond pas aux méthodes habituelles de calcul du centile. Si vous calculez le centile comme "proportion de points de données strictement inférieurs à cette valeur", la valeur maximale doit être de 0,8 (étant donné que 4 valeurs sur 5 sont inférieures à la plus grande). Si vous la calculez en "pourcentage de points de données inférieurs ou égaux à cette valeur", la valeur inférieure doit être de 0,2 (étant donné qu'une des cinq valeurs est égale à la plus petite). Ainsi, les centiles seraient [0, 0.2, 0.4, 0.6, 0.8] ou [0.2, 0.4, 0.6, 0.8, 1]. Votre définition semble être "le nombre de points de données strictement inférieur à cette valeur, considéré comme une proportion du nombre de points de données non égal à cette valeur", mais dans mon expérience ce n’est pas une définition courante (voir par exemple Wikipédia ).

Avec les définitions typiques des centiles, le centile d'un point de données est égal à son rang divisé par le nombre de points de données. (Voir par exemple cette question sur les statistiques SE demandant comment faire la même chose dans R.) Différences dans la manière de calculer le centile en différences dans la manière de calculer le rang (par exemple, comment classer des valeurs liées) . La fonction scipy.stats.percentileofscore fournit quatre méthodes de calcul des centiles:

>>> x = [1, 1, 2, 2, 17]
>>> [stats.percentileofscore(x, a, 'rank') for a in x]
[30.0, 30.0, 70.0, 70.0, 100.0]
>>> [stats.percentileofscore(x, a, 'weak') for a in x]
[40.0, 40.0, 80.0, 80.0, 100.0]
>>> [stats.percentileofscore(x, a, 'strict') for a in x]
[0.0, 0.0, 40.0, 40.0, 80.0]
>>> [stats.percentileofscore(x, a, 'mean') for a in x]
[20.0, 20.0, 60.0, 60.0, 90.0]

(J'ai utilisé un ensemble de données contenant des liens pour illustrer ce qui se passe dans de tels cas.)

La méthode "rang" attribue aux groupes à égalité un rang égal à la moyenne des rangs qu’ils couvriraient (c’est-à-dire qu’une égalité à trois pour la 2e place obtient un rang de 3 car elle "occupe" les rangs 2, 3 et 4). La méthode "faible" attribue un centile basé sur la proportion de points de données inférieurs ou égaux à un point donné; "strict" est identique, mais compte la proportion de points strictement inférieur au point donné. La méthode "moyenne" est la moyenne des deux derniers.

Comme l'a noté Kevin H. Lin, appeler percentileofscore dans une boucle est inefficace car il doit recalculer les rangs à chaque passe. Cependant, ces calculs de centiles peuvent être facilement répliqués à l'aide de différentes méthodes de classement fournies par scipy.stats.rankdata , vous permettant de calculer tous les centiles à la fois:

>>> from scipy import stats
>>> stats.rankdata(x, "average")/len(x)
array([ 0.3,  0.3,  0.7,  0.7,  1. ])
>>> stats.rankdata(x, 'max')/len(x)
array([ 0.4,  0.4,  0.8,  0.8,  1. ])
>>> (stats.rankdata(x, 'min')-1)/len(x)
array([ 0. ,  0. ,  0.4,  0.4,  0.8])

Dans le dernier cas, les rangs sont ramenés d'un rang vers le bas pour les faire partir de 0 au lieu de 1. J'ai omis «moyen», mais on pourrait facilement l'obtenir en calculant la moyenne des résultats des deux dernières méthodes.)

J'ai fait des timings. Avec de petites données telles que celle de votre exemple, l'utilisation de rankdata est un peu plus lente que la solution de Kevin H. Lin (probablement en raison de la surcharge de temps générée par la conversion de choses en tableaux numpy sous le capot), mais plus rapide que d'appeler percentileofscore dans une boucle comme dans reptilicus réponse:

In [11]: %timeit [stats.percentileofscore(x, i) for i in x]
1000 loops, best of 3: 414 µs per loop

In [12]: %timeit list_to_percentiles(x)
100000 loops, best of 3: 11.1 µs per loop

In [13]: %timeit stats.rankdata(x, "average")/len(x)
10000 loops, best of 3: 39.3 µs per loop

Avec un jeu de données volumineux, toutefois, l'avantage de performance de numpy prend effet et l'utilisation de rankdata est 10 fois plus rapide que le list_to_percentiles de Kevin:

In [18]: x = np.random.randint(0, 10000, 1000)

In [19]: %timeit [stats.percentileofscore(x, i) for i in x]
1 loops, best of 3: 437 ms per loop

In [20]: %timeit list_to_percentiles(x)
100 loops, best of 3: 1.08 ms per loop

In [21]: %timeit stats.rankdata(x, "average")/len(x)
10000 loops, best of 3: 102 µs per loop

Cet avantage ne deviendra plus prononcé que sur des jeux de données de plus en plus grands.

29
BrenBarn

Je pense que tu veux scipy.stats.percentileofscore

Exemple: 

percentileofscore([1, 2, 3, 4], 3)
75.0
percentiles = [percentileofscore(data, i) for i in data]
14
reptilicus

Version pure numpy de la solution de Kevin

Comme Kevin l'a dit, la solution optimale fonctionne en temps O (n log (n)). Voici la version rapide de son code dans numpy, qui fonctionne presque en même temps que stats.rankdata:

percentiles = numpy.argsort(numpy.argsort(array)) * 100. / (len(array) - 1)

PS Ceci est un si mes astuces préférées dans numpy.

11
Alleo

En termes de complexité, je pense que la réponse de reptilicus n’est pas optimale. Cela prend O (n ^ 2) fois.

Voici une solution qui prend O (n log n).

def list_to_percentiles(numbers):
    pairs = Zip(numbers, range(len(numbers)))
    pairs.sort(key=lambda p: p[0])
    result = [0 for i in range(len(numbers))]
    for rank in xrange(len(numbers)):
        original_index = pairs[rank][1]
        result[original_index] = rank * 100.0 / (len(numbers)-1)
    return result

Je ne suis pas sûr, mais je pense que c'est la complexité temporelle optimale que vous pouvez obtenir. La raison approximative qui me semble optimale est que les informations de tous les centiles sont essentiellement équivalentes à celles de la liste triée et que vous ne pouvez pas obtenir mieux que O (n log n) pour le tri.

EDIT: Selon votre définition du "percentile", cela peut ne pas toujours donner le résultat correct. Voir la réponse de BrenBarn pour plus d'explications et pour une meilleure solution qui utilise scipy/numpy.

9
Kevin H. Lin

cela peut sembler exagéré, mais qu'en est-il de ceci:

def percentile(x):
    pc = float(1)/(len(x)-1)
    return ["%.2f"%(n*pc) for n, i in enumerate(x)]

MODIFIER:

def percentile(x):
    unique = set(x)
    mapping = {}
    pc = float(1)/(len(unique)-1)
    for n, i in enumerate(unique):
        mapping[i] = "%.2f"%(n*pc)
    return [mapping.get(el) for el in x]
2
aschmid00

Si je vous ai bien compris, tout ce que vous voulez faire est de définir le centile que cet élément représente dans le tableau. comme dans [1, 2, 3, 4, 5] devrait être [0,0, 0,25, 0,5, 0,75, 1,0]

Je crois qu'un tel code suffira:

def percentileListEdited(List):
    uniqueList = list(set(List))
    increase = 1.0/(len(uniqueList)-1)
    newList = {}
    for index, value in enumerate(uniqueList):
        newList[index] = 0.0 + increase * index
    return [newList[val] for val in List]
1
Mahmoud Aladdin

Pour moi, la meilleure solution consiste à utiliser QuantileTransformer dans sklearn.preprocessing

from sklearn.preprocessing import QuantileTransformer
fn = lambda input_list : QuantileTransformer(100).fit_transform(np.array(input_list).reshape([-1,1])).ravel().tolist()
input_raw = [1, 2, 3, 4, 17]
output_perc = fn( input_raw )

print "Input=", input_raw
print "Output=", np.round(output_perc,2)

Voici la sortie

Input= [1, 2, 3, 4, 17]
Output= [ 0.    0.25  0.5   0.75  1.  ]

Remarque: cette fonction présente deux caractéristiques principales:

  1. les données brutes en entrée ne sont PAS nécessairement triées.
  2. les données brutes en entrée ne correspondent PAS nécessairement à une seule colonne.
0
pitfall

Cette version permet également de passer les valeurs exactes en centiles utilisées pour le classement: 

def what_pctl_number_of(x, a, pctls=np.arange(1, 101)):
    return np.argmax(np.sign(np.append(np.percentile(x, pctls), np.inf) - a))

Il est donc possible de savoir quelle est la valeur du nombre centile qui tombe pour les centiles fournis: 

_x = np.random.randn(100, 1)
what_pctl_number_of(_x, 1.6, [25, 50, 75, 100])

Sortie:

3

donc il frappe à 75 ~ 100 gamme

0
mde