Итератор скользящего или скользящего окна? - PullRequest
131 голосов
/ 26 июля 2011

Мне нужно скользящее окно (или скользящее окно), повторяемое по последовательности / итератору / генератору. По умолчанию итерацию Python можно рассматривать как особый случай, когда длина окна равна 1. В настоящее время я использую следующий код. У кого-нибудь есть более Pythonic, менее многословный или более эффективный метод для этого?

def rolling_window(seq, window_size):
    it = iter(seq)
    win = [it.next() for cnt in xrange(window_size)] # First window
    yield win
    for e in it: # Subsequent windows
        win[:-1] = win[1:]
        win[-1] = e
        yield win

if __name__=="__main__":
    for w in rolling_window(xrange(6), 3):
        print w

"""Example output:

   [0, 1, 2]
   [1, 2, 3]
   [2, 3, 4]
   [3, 4, 5]
"""

Ответы [ 19 ]

107 голосов
/ 26 июля 2011

В старой версии документов Python есть один с itertools примерами :

from itertools import islice

def window(seq, n=2):
    "Returns a sliding window (of width n) over data from the iterable"
    "   s -> (s0,s1,...s[n-1]), (s1,s2,...,sn), ...                   "
    it = iter(seq)
    result = tuple(islice(it, n))
    if len(result) == n:
        yield result
    for elem in it:
        result = result[1:] + (elem,)
        yield result

Один из документов является более кратким и использует itertools длябольший эффект я представляю.

44 голосов
/ 26 июля 2011

Это выглядит специально для collections.deque, так как у вас по существу есть FIFO (добавьте к одному концу, удалите с другого).Однако, даже если вы используете list, вам не следует нарезать ломтики дважды;вместо этого вы, вероятно, должны просто pop(0) из списка и append() новый элемент.

Вот оптимизированная реализация на основе deque, созданная по образцу вашего оригинала:

from collections import deque

def window(seq, n=2):
    it = iter(seq)
    win = deque((next(it, None) for _ in xrange(n)), maxlen=n)
    yield win
    append = win.append
    for e in it:
        append(e)
        yield win

В моемтестирует, что он легко превосходит все остальное, размещенное здесь, большую часть времени, хотя версия pillmuncher tee превосходит его для больших итераций и маленьких окон.В больших окнах deque снова перемещается вперед с необработанной скоростью.

Доступ к отдельным элементам в deque может быть быстрее или медленнее, чем со списками или кортежами.(Элементы в начале быстрее или элементы в конце, если вы используете отрицательный индекс.) Я поместил sum(w) в тело моего цикла;это играет на прочность deque (итерация от одного элемента к другому выполняется быстро, поэтому этот цикл работает на 20% быстрее, чем следующий самый быстрый метод, pillmuncher's).Когда я изменил его для индивидуального поиска и добавления элементов в окне из десяти, таблицы развернулись, и метод tee оказался на 20% быстрее.Мне удалось восстановить некоторую скорость, используя отрицательные индексы для последних пяти слагаемых в добавлении, но tee все еще был немного быстрее.В целом, я бы оценил, что любой из них достаточно быстр для большинства применений, и если вам нужно немного больше производительности, выберите профиль и выберите тот, который работает лучше всего.

32 голосов
/ 26 июля 2011

Мне нравится tee():

from itertools import tee, izip

def window(iterable, size):
    iters = tee(iterable, size)
    for i in xrange(1, size):
        for each in iters[i:]:
            next(each, None)
    return izip(*iters)

for each in window(xrange(6), 3):
    print list(each)

дает:

[0, 1, 2]
[1, 2, 3]
[2, 3, 4]
[3, 4, 5]
18 голосов
/ 16 ноября 2012

Вот обобщение, в котором добавлена ​​поддержка параметров step, fillvalue:

from collections import deque
from itertools import islice

def sliding_window(iterable, size=2, step=1, fillvalue=None):
    if size < 0 or step < 1:
        raise ValueError
    it = iter(iterable)
    q = deque(islice(it, size), maxlen=size)
    if not q:
        return  # empty iterable or size == 0
    q.extend(fillvalue for _ in range(size - len(q)))  # pad to size
    while True:
        yield iter(q)  # iter() to avoid accidental outside modifications
        try:
            q.append(next(it))
        except StopIteration: # Python 3.5 pep 479 support
            return
        q.extend(next(it, fillvalue) for _ in range(step - 1))

Это дает в виде кусков size элементов за один раз, прокручивая step позиций за итерацию, дополняя каждый кусок с fillvalue, если необходимо. Пример для size=4, step=3, fillvalue='*':

 [a b c d]e f g h i j k l m n o p q r s t u v w x y z
  a b c[d e f g]h i j k l m n o p q r s t u v w x y z
  a b c d e f[g h i j]k l m n o p q r s t u v w x y z
  a b c d e f g h i[j k l m]n o p q r s t u v w x y z
  a b c d e f g h i j k l[m n o p]q r s t u v w x y z
  a b c d e f g h i j k l m n o[p q r s]t u v w x y z
  a b c d e f g h i j k l m n o p q r[s t u v]w x y z
  a b c d e f g h i j k l m n o p q r s t u[v w x y]z
  a b c d e f g h i j k l m n o p q r s t u v w x[y z * *]

Пример использования параметра step приведен в Эффективная обработка большого файла .txt в python .

9 голосов
/ 28 июня 2012

Просто быстрый вклад.

Поскольку в текущих документах по Python в примерах itertool нет «окна» (т. Е. В нижней части http://docs.python.org/library/itertools.html),, здесь приведен фрагмент, основанный на код для группера, который является одним из приведенных примеров:

import itertools as it
def window(iterable, size):
    shiftedStarts = [it.islice(iterable, s, None) for s in xrange(size)]
    return it.izip(*shiftedStarts)

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

Так же, как и в версиях appending-element и advising-iterator, приведенных выше, производительность (то есть лучшая) зависит от размера списка и размера окна. Мне нравится этот, потому что он двухстрочный (может быть однострочным, но я предпочитаю концепции именования).

Оказывается, вышеприведенный код неверен . Это работает, если параметр, переданный в iterable , является последовательностью, но не если это итератор. Если это итератор, один и тот же итератор разделяется (но не tee'd) между вызовами islice, и это сильно нарушает работу.

Вот некоторый фиксированный код:

import itertools as it
def window(iterable, size):
    itrs = it.tee(iterable, size)
    shiftedStarts = [it.islice(anItr, s, None) for s, anItr in enumerate(itrs)]
    return it.izip(*shiftedStarts)

Также еще одна версия для книг. Вместо того, чтобы копировать итератор и затем многократно продвигать копии, эта версия делает попарные копии каждого итератора, когда мы перемещаем начальную позицию вперед. Таким образом, итератор t обеспечивает как «полный» итератор начальной точкой в ​​t, так и основой для создания итератора t + 1:

import itertools as it
def window4(iterable, size):
    complete_itr, incomplete_itr = it.tee(iterable, 2)
    iters = [complete_itr]
    for i in xrange(1, size):
        incomplete_itr.next()
        complete_itr, incomplete_itr = it.tee(incomplete_itr, 2)
        iters.append(complete_itr)
    return it.izip(*iters)
8 голосов
/ 02 декабря 2016

Просто чтобы показать, как вы можете комбинировать itertools рецепты , я расширяю рецепт pairwise как можно напрямую обратно в рецепт window, используя рецепт consume:

def consume(iterator, n):
    "Advance the iterator n-steps ahead. If n is none, consume entirely."
    # Use functions that consume iterators at C speed.
    if n is None:
        # feed the entire iterator into a zero-length deque
        collections.deque(iterator, maxlen=0)
    else:
        # advance to the empty slice starting at position n
        next(islice(iterator, n, n), None)

def window(iterable, n=2):
    "s -> (s0, ...,s(n-1)), (s1, ...,sn), (s2, ..., s(n+1)), ..."
    iters = tee(iterable, n)
    # Could use enumerate(islice(iters, 1, None), 1) to avoid consume(it, 0), but that's
    # slower for larger window sizes, while saving only small fixed "noop" cost
    for i, it in enumerate(iters):
        consume(it, i)
    return zip(*iters)

Рецепт window такой же, как и для pairwise, он просто заменяет отдельный элемент "потребление" на втором итераторе tee с прогрессивным увеличением потребления на итераторах n - 1.Использование consume вместо переноса каждого итератора в islice незначительно быстрее (для достаточно больших итераций), поскольку вы оплачиваете только издержки переноса islice во время фазы consume, а не во время извлечения каждого значения, редактируемого в окне(поэтому он ограничен n, а не количеством элементов в iterable).

По производительности, по сравнению с некоторыми другими решениями, это довольно хорошо (и лучше, чем любое другое решение, которое япроверено как оно масштабируется).Протестировано на Python 3.5.0, Linux x86-64, с использованием ipython %timeit magic.

Решение deque kindall , настроенное на производительность / корректность с помощью isliceвместо выражения собственного генератора, проверяющего полученную длину, чтобы он не давал результатов, когда итерация короче окна, а также передавая maxlen deque позиционно, а не по ключевому слову (делаетудивительная разница для меньших входов):

>>> %timeit -r5 deque(windowkindall(range(10), 3), 0)
100000 loops, best of 5: 1.87 μs per loop
>>> %timeit -r5 deque(windowkindall(range(1000), 3), 0)
10000 loops, best of 5: 72.6 μs per loop
>>> %timeit -r5 deque(windowkindall(range(1000), 30), 0)
1000 loops, best of 5: 71.6 μs per loop

То же, что и в предыдущем адаптированном решении kindall, но с каждым yield win изменяется на yield tuple(win), поэтому сохранение результатов из генератора работает без того, чтобы все сохраненные результаты действительно были виднысамый последний результат (все другие разумные решения безопасны в этом сценарии) и добавление tuple=tuple к определению функции для перемещения использования tuple с B in LEGB на L:

>>> %timeit -r5 deque(windowkindalltupled(range(10), 3), 0)
100000 loops, best of 5: 3.05 μs per loop
>>> %timeit -r5 deque(windowkindalltupled(range(1000), 3), 0)
10000 loops, best of 5: 207 μs per loop
>>> %timeit -r5 deque(windowkindalltupled(range(1000), 30), 0)
1000 loops, best of 5: 348 μs per loop

consume решение, показанное выше:

>>> %timeit -r5 deque(windowconsume(range(10), 3), 0)
100000 loops, best of 5: 3.92 μs per loop
>>> %timeit -r5 deque(windowconsume(range(1000), 3), 0)
10000 loops, best of 5: 42.8 μs per loop
>>> %timeit -r5 deque(windowconsume(range(1000), 30), 0)
1000 loops, best of 5: 232 μs per loop

То же, что и consume, но вставка else регистр consume, чтобы избежать вызова функции и n is Noneтест для сокращения времени выполнения, особенно для небольших входных данных, где издержки установки являются значимой частью работы:

>>> %timeit -r5 deque(windowinlineconsume(range(10), 3), 0)
100000 loops, best of 5: 3.57 μs per loop
>>> %timeit -r5 deque(windowinlineconsume(range(1000), 3), 0)
10000 loops, best of 5: 40.9 μs per loop
>>> %timeit -r5 deque(windowinlineconsume(range(1000), 30), 0)
1000 loops, best of 5: 211 μs per loop

(Примечание: вариант на pairwise, использующий tee с аргументом по умолчанию)из 2 для создания вложенных tee объектов, поэтому любой данный итератор продвигается только один раз, независимо не потребляется все большее число раз, подобно ответ MrDrFenner аналогичен не встроенному consume и медленнеечем встроенный consume во всех тестах, поэтому я для краткости опускаю эти результаты).

Как вы можете видеть, , если вам не нужно, чтобы вызывающий абонент нуждался всохраняя результаты, моя оптимизированная версия решения kindall выигрывает большую часть времени, за исключением «большого итерируемого, малого размера окна» (где встроенный consume выигрывает);он быстро ухудшается при увеличении итеративного размера, но не уменьшается вообще при увеличении размера окна (любое другое решение ухудшается медленнее при увеличении итеративного размера, но также ухудшается при увеличении размера окна).Его даже можно адаптировать к случаю «нужных кортежей», добавив map(tuple, ...), который работает немного медленнее, чем добавление кортежей в функцию, но тривиален (занимает на 1-5% больше) и позволяет сохранять гибкостьработать быстрее, когда вы можете допускать многократное возвращение одного и того же значения.

Если вам требуется безопасность при сохранении возвратов, встроенный consume выигрывает на всех входных размерах, кроме самых маленьких (с неconsume немного медленнее, но масштабируется аналогично).Решение на основе deque & tupling выигрывает только для наименьших входных данных из-за меньших затрат на установку и небольшого усиления;он сильно ухудшается по мере увеличения длины итерации.

Для протокола, адаптированная версия решения kindall, которую я использовал yield s tuple s, была:

def windowkindalltupled(iterable, n=2, tuple=tuple):
    it = iter(iterable)
    win = deque(islice(it, n), n)
    if len(win) < n:
        return
    append = win.append
    yield tuple(win)
    for e in it:
        append(e)
        yield tuple(win)

Отбросьте кешированиеtuple в строке определения функции и использование tuple в каждом yield, чтобы получить более быструю, но менее безопасную версию.

7 голосов
/ 25 сентября 2017

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

import more_itertools
list(more_itertools.windowed([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],n=3, step=3))

Out: [(1, 2, 3), (4, 5, 6), (7, 8, 9), (10, 11, 12), (13, 14, 15)]
7 голосов
/ 26 марта 2012

Я использую следующий код в качестве простого скользящего окна, которое использует генераторы для значительного повышения читабельности. По моему опыту, его скорость была достаточной для использования в анализе последовательности биоинформатики.

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

def slidingWindow(sequence,winSize,step=1):
"""Returns a generator that will iterate through
the defined chunks of input sequence. Input sequence
must be sliceable."""

    # Verify the inputs
    if not ((type(winSize) == type(0)) and (type(step) == type(0))):
        raise Exception("**ERROR** type(winSize) and type(step) must be int.")
    if step > winSize:
        raise Exception("**ERROR** step must not be larger than winSize.")
    if winSize > len(sequence):
        raise Exception("**ERROR** winSize must not be larger than sequence length.")

    # Pre-compute number of chunks to emit
    numOfChunks = ((len(sequence)-winSize)/step)+1

    # Do the work
    for i in range(0,numOfChunks*step,step):
        yield sequence[i:i+winSize]
6 голосов
/ 28 июля 2016
def GetShiftingWindows(thelist, size):
    return [ thelist[x:x+size] for x in range( len(thelist) - size + 1 ) ]

>> a = [1, 2, 3, 4, 5]
>> GetShiftingWindows(a, 3)
[ [1, 2, 3], [2, 3, 4], [3, 4, 5] ]
5 голосов
/ 24 июля 2013

слегка измененная версия окна deque, чтобы сделать его настоящим скользящим окном.Так что он начинает заполняться только одним элементом, затем увеличивается до максимального размера окна, а затем сжимается, когда его левый край приближается к концу:

from collections import deque
def window(seq, n=2):
    it = iter(seq)
    win = deque((next(it, None) for _ in xrange(1)), maxlen=n)
    yield win
    append = win.append
    for e in it:
        append(e)
        yield win
    for _ in xrange(len(win)-1):
        win.popleft()
        yield win

for wnd in window(range(5), n=3):
    print(list(wnd))

это дает

[0]
[0, 1]
[0, 1, 2]
[1, 2, 3]
[2, 3, 4]
[3, 4]
[4]
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...