Itérateur de fenêtre mobile ou coulissant?

j'ai besoin d'une fenêtre roulante (alias fenêtre coulissante) itérable sur une séquence/itérateur/générateur. L'itération Python par défaut peut être considérée comme un cas spécial, où la longueur de la fenêtre est 1. J'utilise actuellement le code suivant. Est-ce que quelqu'un a une méthode plus pythonique, moins verbeuse, ou plus efficace pour faire cela?

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]
"""
117
demandé sur martineau 2011-07-26 01:41:58

19 réponses

il y en a un dans une ancienne version de Python docs avec itertools exemples :

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

celui du docs est un peu plus succinct et utilise itertools pour plus d'effet j'imagine.

94
répondu Daniel DiPaolo 2017-09-04 19:51:30

cela semble taillé sur mesure pour un collections.deque puisque vous avez essentiellement un FIFO (ajouter à un bout, enlever de l'autre). Cependant, même si vous utilisez un list , vous ne devriez pas couper deux fois; à la place, vous devriez probablement juste pop(0) de la liste et append() le nouvel article.

Voici une implémentation basée sur le deque optimisée, inspirée de votre original:

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

dans mes tests, il bat handily tout le reste affiché ici la plupart du temps, bien que la version tee de pillmuncher bat pour les grandes itérables et les petites fenêtres. Sur les fenêtres plus grandes, le deque prend de nouveau de l'avance en vitesse brute.

L'accès aux éléments individuels du deque peut être plus rapide ou plus lent qu'avec les listes ou les tuples. (Les éléments près du début sont plus rapides, ou les éléments près de la fin si vous utilisez un indice négatif.) J'ai mis un sum(w) dans le corps de ma boucle; cela joue à la force de la deque (itérer d'un élément à l'autre est rapide, de sorte que cette boucle a couru un 20% plus rapide que la méthode la plus rapide suivante, pillmuncher's). Quand je l'ai changé pour chercher individuellement et ajouter des articles dans une fenêtre de dix, les tables ont tourné et la méthode tee était 20% plus rapide. J'ai pu récupérer un peu de vitesse en utilisant des indices négatifs pour les cinq derniers termes dans l'addition, mais tee était encore un peu plus rapide. Globalement, je dirais que l'un est beaucoup rapide pour la plupart des utilisations et si vous besoin d'un peu plus de performance, de profil et de choisir celui qui fonctionne le mieux.

43
répondu kindall 2011-07-27 21:36:32

j'aime 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)

donne:

[0, 1, 2]
[1, 2, 3]
[2, 3, 4]
[3, 4, 5]
28
répondu pillmuncher 2011-07-25 22:19:10

Voici une généralisation qui ajoute la prise en charge des paramètres 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))

Il produit en morceaux size éléments à la fois de rouler step positions par itération remplissage de chaque morceau avec fillvalue si nécessaire. Exemple pour 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 * *]

pour un exemple d'utilisation du paramètre step , voir traitement d'une grande taille .fichier txt en python efficacement .

16
répondu jfs 2017-05-23 12:03:02

juste une contribution rapide.

puisque les docs python actuels n'ont pas de "fenêtre" dans les exemples itertool (i.e., au bas de http://docs.python.org/library/itertools.html ), voici un extrait basé sur le code pour mérou qui est l'un des exemples donnés:

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

Fondamentalement, nous créons une série d'itérateurs tranchés, chacun avec un point de départ un point plus loin en avant. Ensuite, nous zip ces ainsi. Remarque, cette fonction renvoie un générateur (il n'est pas directement un générateur lui-même).

tout comme les versions des itérateurs et des éléments auxiliaires ci-dessus, la performance (c.-à-d. la meilleure) varie en fonction de la taille de la liste et de la taille de la fenêtre. Je l'aime parce que c'est un deux-liner (cela pourrait être un one-liner, mais je préfère l'appellation de concepts).

il s'avère que le code ci-dessus est faux . Il fonctionne si le paramètre passé itératif est une séquence, mais pas si c'est un itérateur. Si c'est un iterator, le même iterator est partagé (mais pas tee'd) entre les appels d'islice et cela casse les choses mal.

voici un code fixe:

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)

aussi, une version de plus pour les livres. Au lieu de copier un itérateur puis d'avancer des copies de nombreuses fois, cette version fait des copies en paires de chaque itérateur que nous déplacer la position de départ transmettre. Ainsi, l'itérateur t fournit à la fois l'itérateur" complet "avec le point de départ à t et aussi la base pour créer l'itérateur 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
répondu MrDrFenner 2012-09-28 05:12:07

juste pour montrer comment vous pouvez combiner itertools recettes , je prolonge la pairwise recette aussi directement que possible dans la window recette en utilisant la consume recette:

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)

la recette window est la même que pour pairwise , il remplace simplement l'élément unique "consommer" sur le second itérateur tee - ed avec des consommations progressivement croissantes sur les itérateurs n - 1 . Utiliser consume au lieu d'envelopper chaque itérateur dans islice est légèrement plus rapide (pour les itérables suffisamment grands) puisque vous payez seulement le islice enveloppant au-dessus pendant la phase consume , pas pendant le processus d'extraction de chaque valeur fenêtre-ed (donc il est limité par n , pas le nombre d'articles dans iterable ).

du point de vue de la Performance, comparé à d'autres solutions, c'est assez bon (et meilleur que n'importe laquelle des autres solutions I testé échelle). Testé sur Python 3.5.0, Linux x86-64, en utilisant ipython %timeit magic.

kindall la deque solution , ajustée pour la performance / exactitude en utilisant islice au lieu d'une expression de générateur laminé à plat et tester la longueur résultante de sorte qu'il ne donne pas de résultats lorsque l'itérable est plus courte que la fenêtre, ainsi que passer le maxlen de la deque positionnellement au lieu de par mot-clé (fait une différence surprenante pour les entrées plus petites):

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

identique à la solution kindall précédemment adaptée, mais avec chaque yield win changé en yield tuple(win) donc stocker les résultats du générateur fonctionne sans tous les résultats stockés étant vraiment une vue du résultat le plus récent (toutes les autres solutions raisonnables sont sûres dans ce scénario), et l'ajout de tuple=tuple à la définition de la fonction de déplacer l'utilisation de tuple de la B en LEGB à la 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 - solution basée ci-dessus:

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

identique à consume , mais inlining else cas de consume pour éviter l'appel de fonction et n is None test pour réduire le temps d'exécution, en particulier pour les petites entrées où la mise au plafond est une partie significative du travail:

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

(note: Une variante de pairwise qui utilise tee avec l'argument par défaut de 2 à plusieurs reprises pour faire des objets imbriqués tee , de sorte qu'un itérateur donné n'est avancé qu'une seule fois, non consommé indépendamment un nombre croissant de fois, similaire à la réponse de MrDrFenner est similaire à non-inlined consume et plus lent que le inlined consume sur tous les tests, donc je l'ai omis ces résultats pour la brièveté).

comme vous pouvez le voir, si vous ne vous souciez pas de la possibilité de l'appelant ayant besoin de stocker des résultats, ma version optimisée de la solution de kindall gagne la plupart du temps, sauf dans le "grand itérable, petite fenêtre de taille" "(où inlined consume gagne); il se dégrade rapidement que la taille itérable augmente, tout en ne se dégradant pas du tout que la taille de la fenêtre augmente (chaque autre solution se dégrade plus lentement pour les augmentations itérables de taille, mais se dégrade également pour les augmentations de taille de fenêtre). Il peut même être adapté pour l'étui "need tuples" en l'enveloppant dans map(tuple, ...) , qui court toujours un peu plus lentement que la mise du tupling dans la fonction, mais il est trivial (prend 1-5% de plus) et vous permet de garder la flexibilité de courir plus vite quand vous pouvez tolérer plusieurs fois retourner la même valeur.

si vous avez besoin de sécurité contre les retours stockés, inlined consume gagne sur toutes les tailles d'entrée sauf les plus petites (avec non-inlined consume étant légèrement plus lent mais échelle similaire). Le deque & tupling solution basée gagne seulement pour les plus petites entrées, en raison de coûts de configuration plus faibles, et le gain est faible; il se dégrade mal que le itérable devient plus long.

Pour l'enregistrement, la version adaptée de kindall la solution yield s tuple s que j'ai utilisé était:

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)

supprimer la mise en cache de tuple dans la ligne de définition de la fonction et l'utilisation de tuple dans chaque yield pour obtenir le plus rapide mais moins version sécurité.

7
répondu ShadowRanger 2017-05-23 11:54:46

j'utilise le code suivant comme une fenêtre coulissante simple qui utilise des générateurs pour augmenter considérablement la lisibilité. D'après mon expérience, sa vitesse est suffisante pour l'analyse des séquences bioinformatiques.

Je l'inclus ici parce que je n'ai pas encore vu cette méthode utilisée. Encore une fois, je ne fais aucune réclamation au sujet de sa performance comparée.

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
répondu Gus 2012-03-26 19:01:22

une version légèrement modifiée de la fenêtre deque, pour en faire une vraie fenêtre roulante. De sorte qu'il commence à être peuplé avec juste un élément, puis grandit à sa taille de fenêtre maximale, et puis se rétrécit comme il est bord gauche vient près de la fin:

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

ce qui donne

[0]
[0, 1]
[0, 1, 2]
[1, 2, 3]
[2, 3, 4]
[3, 4]
[4]
5
répondu Dmitry Avtonomov 2013-12-05 09:02:13
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] ]
4
répondu heyyou482 2016-07-28 03:54:54

plusieurs itérateurs!

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) soulève StopIteration quand la séquence est terminée, et pour une raison cool qui me dépasse, l'instruction de rendement ici l'exclut et la fonction retourne, ignorant les valeurs restantes qui ne forment pas une fenêtre complète.

quoi qu'il en soit, il s'agit de la solution la moins-lignes encore dont la seule exigence est que seq mettre en œuvre soit __iter__ ou __getitem__ et ne repose pas sur itertools ou collections en plus de la solution de @dansalmo:)

2
répondu jameh 2013-10-21 04:40:15
def rolling_window(list, degree):
    for i in range(len(list)-degree+1):
        yield [list[i+o] for o in range(degree)]

Fait ceci pour une moyenne de la fonction

2
répondu yazdmich 2014-09-29 22:14:08

pourquoi pas

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

Il est documenté en Python doc . Vous pouvez facilement l'étendre à une fenêtre plus large.

2
répondu WeiChing Lin 2014-12-28 00:37:27

il y a une bibliothèque qui fait exactement ce dont vous avez besoin:

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)]
2
répondu Nikolay Frick 2017-09-25 18:58:43

le paquet toolz a une fonction sliding_window .

1
répondu W.P. McNeill 2017-07-23 21:46:32
>>> 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
répondu dansalmo 2013-07-24 01:07:53

Que Diriez-vous d'utiliser ce qui suit:

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)

sortie:

[(1, 2, 3), (2, 3, 4), (3, 4, 5), (4, 5, 6), (5, 6, 7)]
0
répondu keocra 2016-04-06 12:42:09

C'est une vieille question mais pour ceux encore intéressés il y a une grande implémentation d'un glisseur de fenêtre utilisant des générateurs dans cette page (par Adrian Rosebrock).

il s'agit d'une implémentation pour OpenCV mais vous pouvez facilement l'utiliser à d'autres fins. Pour les impatients, je vais coller le code ici, mais pour mieux le comprendre, je recommande de visiter la page originale.

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

Tip: vous pouvez vérifier le .shape de la fenêtre lors de l'itération du générateur pour écarter ceux qui ne répondent pas à vos exigences

Cheers

0
répondu DarkCygnus 2017-03-23 23:13:04

Modifié DiPaolo la réponse de pour permettre à l'arbitraire de remplir et de l'étape de variable de taille

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
répondu shouldsee 2018-06-30 22:13:07
#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]

"""

0
répondu FAYAZ 2018-08-02 13:52:46