Алгоритмы Numpy векторизации, чтобы найти первый элемент будущего больше, чем текущий элемент - PullRequest
4 голосов
/ 26 октября 2011

У меня есть временной ряд A. Я хочу сгенерировать другой временной ряд B, такой что

B [i] = j, где j - первый индекс, больший, чем i, такой, что A [j]>А [I].

Есть ли быстрый способ сделать это в NumPy?

Спасибо.

[EDITED]: желательно использовать только O (n) пробела.

Ответы [ 2 ]

9 голосов
/ 26 октября 2011

Недостаточно протестировано, используйте на свой страх и риск.

import numpy

a = numpy.random.random(100)

# a_by_a[i,j] = a[i] > a[j]
a_by_a = a[numpy.newaxis,:] > a[:,numpy.newaxis]
# by taking the upper triangular, we ignore all cases where i < j
a_by_a = numpy.triu(a_by_a)
# argmax will give the first index with the highest value (1 in this case)
print numpy.argmax(a_by_a, axis = 1)

Меньшая версия памяти:

a = numpy.random.random(100)

@numpy.vectorize
def values(i):
    try:
        return (a[i:] > a[i]).nonzero()[0][0] + i
    except IndexError:
        return -1 # no valid values found

b = values(numpy.arange(100))

Более быстрая версия:

@np.vectorize
def values(i):
    try:
        return next(idx for idx, value in enumerate(A[i+1:]) if value > A[i]) + i + 1
    except StopIteration:
        return -1 # no valid values found
return values(np.arange(len(A)))

Еще более быстрая версия:

def future6(A):
    # list of tuples (index into A, value in A)
    # invariant: indexes and values in sorted order
    known = []
    result = []
    for idx in xrange(len(A) - 1, -1, -1):
        value = A[idx]
        # since known is sorted a binary search could be applied here
        # I haven't bothered

        # anything lower then the current value
        # cannot possibly be used again, since this value will be first index instead
        # of those
        known = [(x,y) for x,y in known if y > value]


        if known: 
            # all values in known are > current value
            # they reverse sorted by index               
            # the value with the lowest index is first
            result.append(known[-1][0])
        else:
            # no values exist this high, report -1
            result.append(-1)
        # add to end of the list to maintain invariant
        known.append( (idx, value) )

    # let numpy worry about reversing the array
    return np.array(result)[::-1]

Благодарим киборга за некоторые идеи, использованные здесь.

Разница в алгоритмах

Киборг показал значительные различия между различными алгоритмами в зависимости от данных, вводимых в них.Я собрал некоторые статистические данные из запущенных алгоритмов, чтобы посмотреть, смогу ли я выяснить, что происходит.

Случайные данные:

Average distance between value and its target: 9
Average length of ascends list: 24
Average length of segment in ascends list: 1.33
Average length of known list: 9.1

Поскольку списки действительно короткие, алгоритм восхождений в основном распадается на линейный поиск.Он очищает подъемы, которые не могут быть использованы в будущем, поэтому он все же заметно лучше, чем линейный поиск.

Колеблющийся:

Average distance between value and its target: 31.46
Average length of ascends list: 84
Average length of segment in ascends list: 1.70
Average length of known list: 57.98

Колебания имеют тенденцию кРазместите разные части дальше друг от друга.Это естественно затрудняет алгоритм линейного поиска.Оба «умных» алгоритма должны отслеживать дополнительные данные.Мой алгоритм значительно затухает, поскольку каждый раз сканирует данные.Алгоритм возрастания затрагивает меньше своих данных и работает лучше.

Ascending:

Average distance between value and its target: 2.57
Average length of ascends list: 40
Average length of segment in ascends list: 3.27
Average length of known list: 3037.97

Понятно, почему у моего алгоритма есть проблемы, он должен отслеживать огромное количествовосходящих значений.Короткое расстояние между значением и целью объясняет хорошую производительность линейного поиска.Ascends до сих пор не работает с очень длинными сегментами.

Лучшие алгоритмы

У моего алгоритма нет причин для линейного поиска по данным.Он отсортирован, и нам просто нужно удалить небольшие значения из конца списка.

def future6(A):
    # list of tuples (index into A, value in A)
    # invariant: indexes and values in sorted order
    known = []
    result = []
    for idx in xrange(len(A) - 1, -1, -1):
        value = A[idx]
        # since known is sorted a binary search could be applied here
        # I haven't bothered

        # anything lower then the current value
        # cannot possibly be used again, since this value will be first index instead
        # of those
        while known and known[-1][1] < value:
            known.pop()


        if known: 
            # all values in known are > current value
            # they reverse sorted by index               
            # the value with the lowest index is first
            result.append(known[-1][0])
        else:
            # no values exist this high, report -1
            result.append(-1)
        # add to end of the list to maintain invariant
        known.append( (idx, value) )

    # let numpy worry about reversing the array
    return np.array(result)[::-1]

Но мне приходит в голову, что мы можем повторно использовать предыдущие вычисленные значения B вместо создания новых структур данных.Если j> i, A [i]> A [j], то B [i]> B [j].

def future8(A):
    B = [-1] * len(A)
    for index in xrange(len(A)-2, -1, -1):
        target = index + 1
        value = A[index]
        while target != -1 and A[target] < value:
            target = B[target]
        B[index] = target
    return np.array(B)

Мои результаты тестов:

Random series:
future2 ascends  : 0.242569923401
future6 full list: 0.0363488197327
future7 vectorize: 0.129994153976
future8 reuse: 0.0299410820007
Oscillating series:
future2 ascends  : 0.233623981476
future6 full list: 0.0360488891602
future7 vectorize: 1.19140791893
future8 reuse: 0.0297570228577
Ascending trend series:
future2 ascends  : 0.120707035065
future6 full list: 0.0314049720764
future7 vectorize: 0.0640320777893
future8 reuse: 0.0246520042419

Восходящие секции

У Киборга была очень интересная идея воспользоваться преимуществами восходящих сегментов.Я не думаю, что какой-либо из его тестовых примеров действительно демонстрирует поведение, которое он преследовал после этого.Я не думаю, что секции были достаточно длинными, чтобы воспользоваться.Но я полагаю, что реальные данные вполне могут иметь такие разделы, поэтому использование их было бы очень полезно.

Но я не думаю, что это сработает.Требуется O (n) время, чтобы подготовить необходимые данные для выполнения двоичного поиска.Это было бы хорошо, если мы выполняем бинарный поиск много раз, но как только мы находим значение в середине восходящего раздела, мы никогда не возвращаемся к чему-либо слева.В результате, даже при бинарном поиске мы тратим не более O (n) времени на обработку данных.

Это может сработать, если будет дешевле построить необходимые данные, чем потом сканировать восходящий участок.Но сканирование довольно дешевое, и вам будет сложно найти способ обработки восходящих участков, который дешевле.

2 голосов
/ 27 октября 2011

@ Метод future8 Уинстона Эверта - O (n) (!), И он лучше, чем все наши предыдущие предложения здесь. Чтобы доказать, что это O (n), обратите внимание, что внутренний цикл while выполняется максимум один раз для любого значения B[target].

Мой старый ответ:

Вот пример трех подходов (после пинг-понга между @Winston Ewert и мной):

  1. Восходящий список с бинарным поиском. (Future2)
  2. Полный список (future6, @Winston Ewert)
  3. numpy.vectorize (future7, улучшенная версия @Winston Ewert's future5).

Каждый из них значительно быстрее, чем другие в разных условиях. Если ряд случайный, то «полный список» (future6) самый быстрый. Если ряд колеблется, то «восходящий список» (future2) является самым быстрым. Если ряд имеет тенденцию к возрастанию, то векторизация (future7) будет самой быстрой.

Если данные представляют собой котировки акций, я бы выбрал «векторизацию» (future7), потому что акции имеют восходящий тренд, и потому что это просто и работает разумно при любых условиях.

Выход:

Random series:
future2 ascends  : 0.210215095646
future6 full list: 0.0920153693145
future7 vectorize: 0.138747922771
Oscillating series:
future2 ascends  : 0.208349650159
future6 full list: 0.940276050999
future7 vectorize: 0.597290143496
Ascending trend series:
future2 ascends  : 0.131106233627
future6 full list: 20.7201531342
future7 vectorize: 0.0540951244451

Код:

import numpy as np
import time 
import timeit

def future2(A):    
    def reverse_enum(L):
        for index in reversed(xrange(len(L))):
            yield len(L)-index-1, L[index]
    def findnext(x, A, ascends): # find index of first future number greater than x
        for idx, segment in reverse_enum(ascends):
            joff=A[segment[0]:segment[1]+1].searchsorted(x,side='right') # binary search
            if joff < (segment[1]-segment[0]+1):
                j=segment[0]+joff
                [ascends.pop() for _ in range(idx)] # delete previous segments
                segment[0]=j # cut beginning of segment 
                return j
        return -1
    B = np.arange(len(A))+1
    # Note: B[i]=-1 where there is no greater value in the future.
    B[-1] = -1 # put -1 at the end
    ascends = [] # a list of pairs of indexes, ascending segments of A
    maximum = True
    for i in xrange(len(A)-2,-1,-1): # scan backwards
        #print(ascends)
        if A[i] < A[i+1]:
            if maximum:
                ascends.append([i+1,i+1])
                maximum = False
            else:
                ascends[-1][0] = i+1
        else:# A[i] >= A[i+1]
            B[i] = findnext(A[i], A, ascends)
            maximum = True
    return B


def future6(A):
    # list of tuples (index into A, value in A)
    # invariant: indexes and values in sorted order
    known = []
    result = []
    for idx in xrange(len(A) - 1, -1, -1):
        value = A[idx]
        # since known is sorted a binary search could be applied here
        # I haven't bothered

        # anything lower then the current value
        # cannot possibly be used again, since this value will be first index instead
        # of those
        known = [(x,y) for x,y in known if y > value]


        if known: 
            # all values in known are > current value
            # they reverse sorted by index               
            # the value with the lowest index is first
            result.append(known[-1][0])
        else:
            # no values exist this high, report -1
            result.append(-1)
        # add to end of the list to maintain invariant
        known.append( (idx, value) )

    # let numpy worry about reversing the array
    return np.array(result)[::-1]


def future7(A):
    @np.vectorize
    def values(i):
        for idx, v in enumerate(A[i+1:]): # loop is faster than genexp with exception
            if A[i]<v:
                return idx+i+1
        return -1
    return values(np.arange(len(A)))

if __name__ == '__main__':
    print('Random series:')
    tsetup = """import future; import numpy; A = numpy.random.random(1e4)"""
    t = timeit.timeit('future.future2(A)', tsetup, number=3)
    print('future2 ascends  : '+str(t))
    t = timeit.timeit('future.future6(A)', tsetup, number=3)
    print('future6 full list: '+str(t))
    t = timeit.timeit('future.future7(A)', tsetup, number=3)
    print('future7 vectorize: '+str(t))

    print('Oscillating series:')
    tsetup = """import future; import numpy; A = numpy.random.randint(1e5,size=1e4)-5e4; A = A.cumsum()"""
    t = timeit.timeit('future.future2(A)', tsetup, number=3)
    print('future2 ascends  : '+str(t))
    t = timeit.timeit('future.future6(A)', tsetup, number=3)
    print('future6 full list: '+str(t))
    t = timeit.timeit('future.future7(A)', tsetup, number=3)
    print('future7 vectorize: '+str(t))

    print('Ascending trend series:')
    tsetup = """import future; import numpy; A = numpy.random.randint(1e5,size=1e4)-3e4; A = A.cumsum()"""
    t = timeit.timeit('future.future2(A)', tsetup, number=3)
    print('future2 ascends  : '+str(t))
    t = timeit.timeit('future.future6(A)', tsetup, number=3)
    print('future6 full list: '+str(t))
    t = timeit.timeit('future.future7(A)', tsetup, number=3)
    print('future7 vectorize: '+str(t))
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...