Как ускорить вложенный цикл python?

Я выполняю вложенный цикл в python, который включен ниже. Это служит основным способом поиска по существующим финансовым временным рядам и поиска периодов во временных рядах, которые соответствуют определенным характеристикам. В этом случае существуют два отдельных массива одинакового размера, представляющих собой "закрытие" (т. е. цену актива) и "объем" (т. е. сумму актива, который был обменен за период). Для каждого периода времени я хотел бы смотреть вперед вообще будущие интервалы с длинами от 1 до INTERVAL_LENGTH и посмотреть, если любой из этих интервалов имеют характеристики, которые соответствуют моему поиску (в этом случае отношение близких значений больше, чем 1.0001 и меньше, чем 1.5 и суммированный объем больше, чем 100).

Я понимаю, что одна из основных причин ускорения при использовании NumPy заключается в том, что интерпретатору не нужно проверять операнды каждый раз, когда он оценивает что-то, пока вы работаете на массив в целом (например, numpy_array * 2), но, очевидно, приведенный ниже код не использует это. Есть ли способ заменить внутренний цикл какой-то оконной функцией, которая может привести к ускорению, или любым другим способом, используя numpy/scipy, чтобы существенно ускорить это в родном python?

альтернативно, есть ли лучший способ сделать это в целом (например, будет ли намного быстрее написать этот цикл на C++ и использовать weave)?

ARRAY_LENGTH = 500000
INTERVAL_LENGTH = 15
close = np.array( xrange(ARRAY_LENGTH) )
volume = np.array( xrange(ARRAY_LENGTH) )
close, volume = close.astype('float64'), volume.astype('float64')

results = []
for i in xrange(len(close) - INTERVAL_LENGTH):
    for j in xrange(i+1, i+INTERVAL_LENGTH):
        ret = close[j] / close[i]
        vol = sum( volume[i+1:j+1] )
        if ret > 1.0001 and ret < 1.5 and vol > 100:
            results.append( [i, j, ret, vol] )
print results

3 ответов


Update: (почти) полностью векторизованная версия ниже в "new_function2"...

Я добавлю комментарии, чтобы объяснить вещи немного.

это дает ускорение ~50x, и большее ускорение возможно, если вы в порядке с выходом, являющимся массивами numpy вместо списков. Как есть:

In [86]: %timeit new_function2(close, volume, INTERVAL_LENGTH)
1 loops, best of 3: 1.15 s per loop

вы можете заменить свой внутренний цикл вызовом np.cumsum()... См. мою функцию" new_function " ниже. Это дает значительное ускорение...

In [61]: %timeit new_function(close, volume, INTERVAL_LENGTH)
1 loops, best of 3: 15.7 s per loop

vs

In [62]: %timeit old_function(close, volume, INTERVAL_LENGTH)
1 loops, best of 3: 53.1 s per loop

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

import numpy as np

ARRAY_LENGTH = 500000
INTERVAL_LENGTH = 15
close = np.arange(ARRAY_LENGTH, dtype=np.float)
volume = np.arange(ARRAY_LENGTH, dtype=np.float)

def old_function(close, volume, INTERVAL_LENGTH):
    results = []
    for i in xrange(len(close) - INTERVAL_LENGTH):
        for j in xrange(i+1, i+INTERVAL_LENGTH):
            ret = close[j] / close[i]
            vol = sum( volume[i+1:j+1] )
            if (ret > 1.0001) and (ret < 1.5) and (vol > 100):
                results.append( (i, j, ret, vol) )
    return results


def new_function(close, volume, INTERVAL_LENGTH):
    results = []
    for i in xrange(close.size - INTERVAL_LENGTH):
        vol = volume[i+1:i+INTERVAL_LENGTH].cumsum()
        ret = close[i+1:i+INTERVAL_LENGTH] / close[i]

        filter = (ret > 1.0001) & (ret < 1.5) & (vol > 100)
        j = np.arange(i+1, i+INTERVAL_LENGTH)[filter]

        tmp_results = zip(j.size * [i], j, ret[filter], vol[filter])
        results.extend(tmp_results)
    return results

def new_function2(close, volume, INTERVAL_LENGTH):
    vol, ret = [], []
    I, J = [], []
    for k in xrange(1, INTERVAL_LENGTH):
        start = k
        end = volume.size - INTERVAL_LENGTH + k
        vol.append(volume[start:end])
        ret.append(close[start:end])
        J.append(np.arange(start, end))
        I.append(np.arange(volume.size - INTERVAL_LENGTH))

    vol = np.vstack(vol)
    ret = np.vstack(ret)
    J = np.vstack(J)
    I = np.vstack(I)

    vol = vol.cumsum(axis=0)
    ret = ret / close[:-INTERVAL_LENGTH]

    filter = (ret > 1.0001) & (ret < 1.5) & (vol > 100)

    vol = vol[filter]
    ret = ret[filter]
    I = I[filter]
    J = J[filter]

    output = zip(I.flat,J.flat,ret.flat,vol.flat)
    return output

results = old_function(close, volume, INTERVAL_LENGTH)
results2 = new_function(close, volume, INTERVAL_LENGTH)
results3 = new_function(close, volume, INTERVAL_LENGTH)

# Using sets to compare, as the output 
# is in a different order than the original function
print set(results) == set(results2)
print set(results) == set(results3)

одним ускорением было бы удалить sum часть, так как в этой реализации он суммирует список длины от 2 до INTERVAL_LENGTH. Вместо этого, просто добавьте volume[j+1] к предыдущему результату vol от последней итерации цикла. Таким образом, вы просто добавляете два целых числа каждый раз, а не суммируете весь список и нарезаете его каждый раз. Кроме того, вместо того, чтобы начать делать sum(volume[i+1:j+1]), просто vol = volume[i+1] + volume[j+1], как вы знаете, начальный случай здесь всегда будет только два индекса.

другой ускорение будет использовать .extend вместо .append, поскольку реализация python имеет extend значительно быстрее работает.

вы также можете разбить финал if оператор, чтобы выполнить только определенные вычисления, если это необходимо. Например, вы знаете if vol <= 100, вам не нужно вычислить ret.

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

Edit-вам также не нужно len, так как вы уже знаете конкретно длину списка (если это не было только для примера). Определение его как числа, а не len(something) всегда быстрее.

Edit-реализация (это непроверено):

ARRAY_LENGTH = 500000
INTERVAL_LENGTH = 15
close = np.array( xrange(ARRAY_LENGTH) )
volume = np.array( xrange(ARRAY_LENGTH) )
close, volume = close.astype('float64'), volume.astype('float64')

results = []
ex = results.extend
for i in xrange(ARRAY_LENGTH - INTERVAL_LENGTH):
    vol = volume[i+1]
    for j in xrange(i+1, i+INTERVAL_LENGTH):
        vol += volume[j+1]
        if vol > 100:
            ret = close[j] / close[i]
            if 1.0001 < ret < 1.5:
                ex( [i, j, ret, vol] )
print results

почему бы вам не попытаться создать результат в виде одного списка (намного быстрее, чем добавление или расширение), что-то вроде:

results = [ t for t in ( (i, j, close[j]/close[i], sum(volume[i+1:j+1]))
                         for i in xrange(len(close)-INT_LEN)
                             for j in xrange(i+1, i+INT_LEN)
                       )
            if t[3] > 100 and 1.0001 < t[2] < 1.5
          ]