滚动或滑动窗口迭代器?


150

我需要在序列/迭代器/生成器上可迭代的滚动窗口(又称滑动窗口)。可以将默认Python迭代视为一种特殊情况,其中窗口长度为1。我当前正在使用以下代码。有没有人有更多的Python风格,更少的冗长或更有效的方法来执行此操作?

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

3
如果您希望在迭代(例如sum()max())时在每个窗口上执行某种操作 ,则需要牢记的是,有有效的算法可以在恒定时间内(不考虑窗口大小)为每个窗口计算新值。我收集了一些这些算法一起在一个Python库:
Alex Riley '18

Answers:


123

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我想它可以起到更大的作用。


2
很好的答案,但是(而且我知道您只是复制链接的配方),我想知道为什么默认窗口大小应该为2?它应该完全没有默认值吗?
SingleNegationElimination 2011年

18
@TakenMacGuy:我不知道该食谱的作者是什么,但是我也选择2。2是最小的有用窗口大小(否则,您只是在迭代并且不需要窗口),这也是常见的需要知道上一个(或下一个)项目,可以说比其他任何特定项目都要多。
kindall 2011年

27
有谁知道为什么将这个示例从文档中删除?这是不是有问题,还是现在有更简单的选择?
2013年


2
一个人for elem in it什么时候会进入循环?
Glassjawed

47

这似乎是为a量身定制的,collections.deque因为您实际上具有FIFO(添加到一端,从另一端移除)。但是,即使使用a list,也不应切片两次;相反,您可能应该只pop(0)从列表和append()新项目开始。

这是一个优化的基于双端队列的实现,该实现以原始格式为基础:

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与中的列表或元组相比,对中单个项目的访问可能更快或更慢。(如果使用负索引,则开头附近的项目会更快,或者结尾附近的项目会更快。)我将a sum(w)放入循环的主体中;这会影响双端队列的强度(从一个项目到另一个项目的迭代速度很快,因此此循环比第二个最快的方法pillmuncher的运行速度快了整整20%)。当我将其更改为单独查找并在10个窗口中添加项目时,表格翻转了,该tee方法快了20%。通过使用最后五个词的负索引,我能够恢复一定的速度,但tee速度仍然要快一些。总的来说,我估计这两种方法对于大多数用途来说都足够快,如果您需要更多性能,请配置并选择最有效的方法。


10
yield win应该是yield tuple(win)yield list(win)防止返回对同一deque对象的引用的迭代器。
乔尔·科内特

1
我将此提交给了PyPI。使用安装pip install sliding_window并运行from sliding_window import window
Thomas Levine 2014年

1
如果您认为list(window(range(10)))应该生成类似[[0,1],[1,2],[2,3],...]的内容,将会感到震惊
Paul

1
显然不会;您需要执行类似操作list(list(x) for x in window(range(10)))或将其添加到迭代器中。对于某些应用程序而言,这很重要,而对于其他应用程序则无关紧要,由于我追求速度,所以我选择了不选择,并在需要时将调用者的责任推给了调用方以复制窗口。
kindall '16

1
如果您将需要的tuple()产量加回去,则该方法与其他方法相比没有任何优势。
kawing-chiu

35

我喜欢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]

从我的快速timeit测试来看,这比Daniel DePaolo的速度要慢得多(大约2:1的比率),并且感觉不那么“尼克尔”。
David B.

@David B .:在我的盒子上,它只比Daniel DePaolo的慢大约8%。
pillmuncher 2011年

@pillmuncher:Python 2.7或3.x?我正在使用2.7。该比率对的值也相当敏感size。如果增加它(例如,如果可迭代的长度为100000个元素,则将窗口大小设置为1000),则可能会看到增加。
David B.

2
@David B .:你说的是有道理的。在我的代码中,的建立时间为itersO(size!),next()多次调用(in izip())可能比复制元组两次更耗时。我使用的是Python 2.6.5,顺便说一句。
pillmuncher 2011年

@pillmuncher:您的意思是,设置时间为itersO(size ^ 2),对吗?
David B.

19

下面是添加了对泛化stepfillvalue参数:

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参数用例的示例,请参阅有效地在python中处理大型.txt文件


16

有一个库可以完全满足您的需求:

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

step=3应该实际删除以符合OP的请求:list(more_itertools.windowed(range(6), 3))
user3780389 '19

9

只是一个快速的贡献。

由于当前的python文档在itertool示例中没有“窗口”(即,位于http://docs.python.org/library/itertools.html的底部),因此下面是基于grouper代码的代码段,是给出的示例之一:

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和advancing-iterator版本一样,性能(即最佳)随列表大小和窗口大小而变化。我喜欢这一行,因为它是两行的(可能是一行,但我更喜欢命名概念)。

原来,以上代码是错误的。如果传递给iterable的参数是一个序列,则有效,但如果它是迭代器,则无效。如果它是一个迭代器,则在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)

9

只是为了展示如何结合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-ed迭代器上逐步增加消耗n - 1迭代器。使用(consume而不是包装每个迭代器islice)速度稍快(对于足够大的可迭代对象),因为您只isliceconsume阶段中支付包装开销,而不是在提取每个窗口值的过程中支付包装开销(因此,它受限制n,而不是其中的项目数限制)iterable)。

在性能方面,与其他一些解决方案相比,这是相当不错的(并且比我测试过的其他任何解决方案都要好)。使用ipython %timeit魔术在Python 3.5.0,Linux x86-64上进行了测试。

kindall的deque解决方案,通过使用islice而不是原始生成器表达式来调整性能/正确性,并测试了结果长度,以便当iterable的长度小于window时,以及maxlendeque位置传递而不是在按关键字(对于较小的输入产生令人惊讶的差异):

>>> %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到函数定义中移动应用tupleBLEGBL

>>> %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&捆绑的解决方案仅因最小的安装成本而获得了成功,因为安装成本较小,并且收益很小。随着迭代时间的延长,它的性能将严重下降。

为了记录在案,kindall的解决方案,它的改编版yield小号tuple的I所用的是:

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以获得更快但不太安全的版本。


显然,这效率不高。consume是通用目的(包括完成任务的能力consume),因此需要额外的导入和每次使用测试n is None。在真正的代码,当且仅当我倒是确定的性能是一个问题,或者我真的需要更简洁的代码,我会考虑内联else的情况下consume进入window,假设我没有使用consume其他的事。但是,如果尚未证明性能是个问题,我将保留单独的定义。命名consume函数使操作少了一些魔术性/自我记录性。
ShadowRanger 2016年

7

我将以下代码用作一个简单的滑动窗口,该窗口使用生成器来大大提高可读性。根据我的经验,到目前为止,它的速度已经足够用于生物信息学序列分析。

我将其包含在此处是因为我尚未看到使用此方法。同样,我对它的比较性能没有任何要求。

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]

3
这里的主要缺点是len(sequence)通话。如果sequence是迭代器或生成器,则将无法使用。当输入确实适合内存时,与迭代器相比,这确实提供了更具可读性的解决方案。
David B.

你是对的。这种特殊情况最初是为了扫描通常表示为字符串的DNA序列。当然确实有您提到的限制。如果需要,您可以简单地测试每个切片以确保其长度正确,然后就不必知道整个序列的长度了。但这会增加一些开销(每次迭代都要进行len()测试)。
Gus 2012年

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

你看“范围(LEN”在Python即时这是一个代码味道。
马克·劳伦斯

@MarkLawrence是什么让您觉得range(lenpython中的错误模式?
duhaime

5

双端队列窗口的略微修改版本,以使其成为真正的滚动窗口。这样它就开始只用一个元素填充,然后增长到最大窗口大小,然后随着左边缘的临近而缩小:

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]

3
def rolling_window(list, degree):
    for i in range(len(list)-degree+1):
        yield [list[i+o] for o in range(degree)]

进行滚动平均功能


3

为什么不

def pairwise(iterable):
    "s -> (s0,s1), (s1,s2), (s2, s3), ..."
    a, b = tee(iterable)
    next(b, None)
    return zip(a, b)

它记录在Python doc中。您可以轻松地将其扩展到更大的窗口。


2

多个迭代器!

def window(seq, size, step=1):
    # initialize iterators
    iters = [iter(seq) for i in range(size)]
    # stagger iterators (without yielding)
    [next(iters[i]) for j in range(size) for i in range(-1, -j-1, -1)]
    while(True):
        yield [next(i) for i in iters]
        # next line does nothing for step = 1 (skips iterations for step > 1)
        [next(i) for i in iters for j in range(step-1)]

next(it)StopIteration当序列完成时引发,并且由于超出我的一些凉爽原因,此处的yield语句除外,并且函数返回,而忽略了未形成完整窗口的剩余值。

无论如何,这是最低线的解决方案还没有,其唯一的要求是seq实现无论是__iter____getitem__和不依赖于itertoolscollections除了@ dansalmo的解决方案:)


注意:交错步骤为O(n ^ 2),其中n是窗口的大小,仅在第一次调用时发生。可以将其优化到O(n),但会使代码更混乱:P
jameh 2013年

2

让我们变得懒惰吧!

from itertools import islice, tee

def window(iterable, size): 
    iterators = tee(iterable, size) 
    iterators = [islice(iterator, i, None) for i, iterator in enumerate(iterators)]  
    yield from zip(*iterators)

list(window(range(5), 3))
# [(0, 1, 2), (1, 2, 3), (2, 3, 4)]

1
#Importing the numpy library
import numpy as np
arr = np.arange(6) #Sequence
window_size = 3
np.lib.stride_tricks.as_strided(arr, shape= (len(arr) - window_size +1, window_size), 
strides = arr.strides*2)

"""Example output:

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

“”


3
请写一些有关您的答案的文字。
jrswgtr

1

我测试了一些解决方案,然后提出了一个解决方案,发现我提出的解决方案是最快的,因此我想与大家分享。

import itertools
import sys

def windowed(l, stride):
    return zip(*[itertools.islice(l, i, sys.maxsize) for i in range(stride)])

1
看起来与该答案中的第一个解决方案相似:stackoverflow.com/a/11249883/7851470
乔治

@georgy我想我跳过了这个答案,因为它是用Python2编写的,但我同意,它基本上是相同的!
Ryan Codrai

0
>>> n, m = 6, 3
>>> k = n - m+1
>>> print ('{}\n'*(k)).format(*[range(i, i+m) for i in xrange(k)])
[0, 1, 2]
[1, 2, 3]
[2, 3, 4]
[3, 4, 5]

0

如何使用以下内容:

mylist = [1, 2, 3, 4, 5, 6, 7]

def sliding_window(l, window_size=2):
    if window_size > len(l):
        raise ValueError("Window size must be smaller or equal to the number of elements in the list.")

    t = []
    for i in xrange(0, window_size):
        t.append(l[i:])

    return zip(*t)

print sliding_window(mylist, 3)

输出:

[(1, 2, 3), (2, 3, 4), (3, 4, 5), (4, 5, 6), (5, 6, 7)]

@ keocra zip(* t)是什么意思?在哪里可以找到有关此类声明的文档?
Shejo284 '17

1
Python 2.7:docs.python.org/2/library/functions.html#zip,星号将列表解压缩并提供各个元素作为zip的输入(解压参数
keocra

0

这是一个古老的问题,但是对于那些仍然感兴趣的人来说,在页面中使用生成器可以很好地实现窗口滑块的实现(作者:Adrian Rosebrock)。

它是OpenCV的一种实现,但是您可以轻松地将其用于其他目的。对于急切的人,我将代码粘贴在这里,但是为了更好地理解它,我建议访问原始页面。

def sliding_window(image, stepSize, windowSize):
    # slide a window across the image
    for y in xrange(0, image.shape[0], stepSize):
        for x in xrange(0, image.shape[1], stepSize):
            # yield the current window
            yield (x, y, image[y:y + windowSize[1], x:x + windowSize[0]])

提示:.shape迭代生成器时,您可以检查窗口的,以丢弃不符合您要求的窗口

干杯


0

修改了DiPaolo的答案以允许任意填充和可变步长

import itertools
def window(seq, n=2,step=1,fill=None,keep=0):
    "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(itertools.islice(it, n))    
    if len(result) == n:
        yield result
    while True:        
#         for elem in it:        
        elem = tuple( next(it, fill) for _ in range(step))
        result = result[step:] + elem        
        if elem[-1] is fill:
            if keep:
                yield result
            break
        yield result

0

这是一个班轮。我对它进行了计时,它与最佳答案的性能相当,并随着seq的增大而逐渐变好,len(seq)= 20时变慢了20%,len(seq)= 10000时变慢了7%

zip(*[seq[i:(len(seq) - n - 1 + i)] for i in range(n)])

请在回答中添加一些解释性文字。并非每个人都绊脚石就是Python Ninja。
Abhijit Sarkar,

等于

0

使用islice尝试我的简单,一种衬里,pythonic方式。但是,可能不是最佳效率。

from itertools import islice
array = range(0, 10)
window_size = 4
map(lambda i: list(islice(array, i, i + window_size)), range(0, len(array) - window_size + 1))
# output = [[0, 1, 2, 3], [1, 2, 3, 4], [2, 3, 4, 5], [3, 4, 5, 6], [4, 5, 6, 7], [5, 6, 7, 8], [6, 7, 8, 9]]

说明:通过使用window_size的islice创建窗口,并使用map在所有数组上进行迭代。


0

深度学习中滑动窗口数据的优化功能

def SlidingWindow(X, window_length, stride):
    indexer = np.arange(window_length)[None, :] + stride*np.arange(int(len(X)/stride)-window_length+4)[:, None]
    return X.take(indexer)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.