Сопоставьте каждое значение списка с соответствующим процентилем

Я хотел бы создать функцию, которая принимает (сортированный) список в качестве аргумента и выводит список, содержащий соответствующий процентиль каждого элемента.

например, fn([1,2,3,4,17]) возвращает [0.0, 0.25, 0.50, 0.75, 1.00].

может кто-нибудь пожалуйста:

  1. помогите мне исправить мой код ниже? Или
  2. предложить лучшую альтернативу, чем мой код для сопоставления значений в списке с их соответствующими процентилями?

мой нынешний код:

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

В настоящее время, если я представлю percentile([1,2,3,4,17]), список [0.0, 0.0, 0.5, 0.0, 1.0] возвращается.

8 ответов


Я думаю, что ваш пример ввода / вывода не соответствует типичным способам вычисления процентиля. Если вы вычисляете процентиль как "доля точек данных строго меньше этого значения", то верхнее значение должно быть 0,8 (так как 4 из 5 значений меньше самого большого). Если вы вычисляете его как "процент точек данных меньше или равен этому значению", то нижнее значение должно быть 0.2 (так как 1 из 5 значений равно наименьшему). Таким образом, процентили будут [0, 0.2, 0.4, 0.6, 0.8] или [0.2, 0.4, 0.6, 0.8, 1]. Ваше определение, по-видимому, "количество точек данных строго меньше этого значения, рассматриваемого как доля числа точек данных, не равных этому значению", но по моему опыту это не общее определение (см., например,Википедия).

при типичных определениях процентилей процентиль точки данных равен ее рангу, деленному на количество точек данных. (См., например,этот вопрос по статистике SE вопрос о том, как сделать то же самое в R.) различия в том, как вычислить процентиль, равны различиям в том, как вычислить ранг (например, как ранжировать привязанные значения). The scipy.stats.percentileofscore функция предоставляет четыре способа вычисления процентилей:

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

(я использовал набор данных, содержащий связи, чтобы проиллюстрировать, что происходит в таких случаях.)

метод "rank" присваивает связанным группам ранг, равный среднему из рангов, которые они будут покрывать (т. е. трехсторонняя связь для 2-го место получает ранг 3, потому что оно "занимает" ряды 2, 3 и 4). "Слабый" метод присваивает процентиль, основанный на доле точек данных, меньших или равных данной точке; "строгий" - это то же самое, но подсчитывает долю точек, строго меньшую, чем данная точка. "Средний" метод-это среднее значение двух последних.

как отметил Кевин Х. Лин, вызывая percentileofscore в цикле неэффективно, так как он должен пересчитать ряды на каждом проходе. Однако эти процентильные расчеты можно легко реплицировать с помощью различных методов ранжирования, предоставляемых scipy.stats.rankdata, позволяя рассчитать все процентили сразу:

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

в последнем случае ряды корректируются на единицу, чтобы они начинались с 0 вместо 1. (Я опустил "среднее", но его можно легко получить, усреднив результаты последних двух методов.)

Я сделал некоторые тайминги. С небольшими данными, такими как в вашем примере, используя rankdata несколько медленнее чем решение Кевина Х. Лина (предположительно из-за накладных расходов scipy происходит при преобразовании вещей в массивы numpy под капотом), но быстрее, чем вызов percentileofscore в цикле, как в ответе рептилии:

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

С большим набором данных, однако, преимущество производительности numpy вступает в силу и использует rankdata в 10 раз быстрее, чем :

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

это преимущество будет только более выраженным на больших и больших наборах данных.


Я думаю, вы хотите scipy.статистика.percentileofscore

пример:

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

чистая версия numpy решения Кевина

как сказал Кевин, оптимальное решение работает в O (N log (n)) времени. Вот быстрая версия его кода numpy, который работает почти в то же время, как stats.rankdata:

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

PS. Это один, если мои любимые трюки в numpy.


С точки зрения сложности, я думаю, что ответ рептиликус не является оптимальным. Это занимает O (n^2) времени.

вот решение, которое занимает 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

Я не уверен, но я думаю, что это оптимальная сложность времени вы можете получить. Грубая причина, по которой я думаю, что это оптимально, заключается в том, что информация всех процентилей по существу эквивалентна информации отсортированного списка, и вы не можете получить лучше, чем O(N log n) для сортировки.

изменить: В зависимости от вашего определения "процентиля" это не всегда может дать правильный результат. См. ответ Бренбарна для большего объяснения и для лучшего решения, которое использует scipy/numpy.


это может выглядеть oversimplyfied, но как насчет этого:

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

EDIT:

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]

Если я правильно вас понимаю, все, что вы хотите сделать, это определить процентиль, который этот элемент представляет в массиве, сколько массива перед этим элементом. как в [1, 2, 3, 4, 5] должно быть [0.0, 0.25, 0.5, 0.75, 1.0]

Я считаю, что такого кода будет достаточно:

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]

для меня лучшим решением является использование QuantileTransformer на 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)

вот вывод

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

Примечание: эта функция имеет две отличительные особенности:

  1. входные необработанные данные не обязательно сортируются.
  2. входные необработанные данные не обязательно являются одним столбцом.

эта версия позволяет также передавать точные процентили значения, используемые для ранжирования:

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

таким образом, можно узнать, какое значение процентиля числа падает на предоставленные процентили:

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

выход:

3

таким образом, он попадает в диапазон 75 ~ 100