174 votes

Itérateur à fenêtre glissante ou roulante en Python

J'ai besoin d'une fenêtre roulante (ou fenêtre coulissante) itérable sur une séquence/un itérateur/un générateur. L'itération Python par défaut peut être considérée comme un cas particulier, où la longueur de la fenêtre est de 1. J'utilise actuellement le code suivant. Quelqu'un a-t-il 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]
"""

5 votes

Si vous cherchez à effectuer une sorte d'opération sur chaque fenêtre au fur et à mesure de l'itération (par ex. sum() ou max() ), il est utile de garder à l'esprit qu'il existe des algorithmes efficaces pour calculer la nouvelle valeur pour chaque fenêtre dans constant temps (indépendamment de la taille de la fenêtre). J'ai rassemblé certains de ces algorithmes dans une bibliothèque Python : en roulant .

135voto

Daniel DiPaolo Points 24085

Il y en a un dans une ancienne version de la documentation Python 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

Celle de la documentation est un peu plus succincte et utilise itertools avec plus d'effet, j'imagine.

2 votes

Belle réponse, mais (et je sais que vous ne faites que reproduire la recette telle qu'elle est liée), je me demande pourquoi la taille de la fenêtre par défaut devrait être de 2. Devrait-elle avoir une valeur par défaut ?

19 votes

@TakenMacGuy : Je ne sais pas quel est le raisonnement de l'auteur de cette recette, mais je choisirais également 2. 2 est la plus petite taille de fenêtre utile (sinon vous ne faites qu'itérer et n'avez pas besoin de la fenêtre), et il est également courant d'avoir besoin de connaître l'élément précédent (ou suivant), sans doute plus que tout autre n spécifique.

30 votes

Quelqu'un sait-il pourquoi cet exemple a été retiré de la documentation ? Y avait-il un problème avec cet exemple ou existe-t-il une alternative plus simple ?

53voto

kindall Points 60645

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

Voici une implémentation optimisée basée sur deque et inspirée de l'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 de loin tout ce qui a été posté ici la plupart du temps, bien que la version de pillmuncher tee l'emporte sur la version pour les grands itérables et les petites fenêtres. Sur les grands Windows, la version deque est de nouveau en tête en vitesse brute.

L'accès aux éléments individuels de la deque peut être plus rapide ou plus lente qu'avec des listes ou des tuples. (Les éléments proches du début sont plus rapides, ou les éléments proches de la fin si vous utilisez un index négatif). Je mets un sum(w) dans le corps de ma boucle ; cela joue sur la force de la deque (itérer d'un élément à l'autre est rapide, donc cette boucle a fonctionné 20% plus vite que la méthode la plus rapide suivante, celle de pillmuncher). Lorsque je l'ai modifié pour rechercher et ajouter des éléments individuellement dans une fenêtre de dix, les choses se sont inversées et la méthode de deque s'est exécutée plus rapidement que celle de pillmuncher. tee était 20% plus rapide. J'ai pu récupérer un peu de vitesse en utilisant des index négatifs pour les cinq derniers termes de l'addition, mais tee était toujours un peu plus rapide. Dans l'ensemble, je dirais que l'un ou l'autre est suffisamment rapide pour la plupart des utilisations et si vous avez besoin d'un peu plus de performances, profilez et choisissez celui qui vous convient le mieux.

0 votes

J'ai failli inclure le collections.deque dans la question originale comme exemple d'une amélioration possible. L'utilisation de pop(0) et append(...) (ou '+') est, bien sûr, bien meilleure. La version de ma question semble indiquer que je pense que les listes sont des tableaux...

0 votes

En fait, en y réfléchissant bien, c'est possible que les frais d'appel de méthode de append() et pop() serait plus lent que le découpage en tranches, surtout pour les petites valeurs. Cette surcharge peut être réduite en assignant une variable locale pour pointer vers ces méthodes.

0 votes

Je vous ai écrit une implémentation basée sur deque et j'ai fait un peu de profilage des différentes solutions postées ici.

38voto

pillmuncher Points 4726

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]

0 votes

De mon rapide timeit Les tests sont beaucoup plus lents que ceux de Daniel DePaolo (dans un rapport d'environ 2:1) et ne sont pas beaucoup plus "agréables".

0 votes

@David B. : Sur mon ordinateur, il est seulement 8% plus lent que celui de Daniel DePaolo.

0 votes

@pillmuncher : Python 2.7 ou 3.x ? J'utilisais la version 2.7. Le rapport est également assez sensible à la valeur de size . Si vous l'augmentez (par exemple, si l'itérable compte 100000 éléments, donnez à la fenêtre une taille de 1000), vous pourrez constater une augmentation.

10voto

Mark Points 113

Juste une petite contribution.

Étant donné que la documentation actuelle de Python ne mentionne pas le mot "window" dans les exemples d'itertool (c.-à-d. au bas de la page d'accueil d'itertool), nous avons décidé de ne pas utiliser le mot "window". http://docs.python.org/library/itertools.html ), voici un extrait basé sur le code de pour grouper 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)

En fait, nous créons une série d'itérateurs découpés, chacun ayant un point de départ un point plus loin. Ensuite, nous les regroupons. Notez que cette fonction renvoie un générateur (ce n'est pas directement un générateur lui-même).

Comme pour les versions appending-element et advancing-iterator ci-dessus, les performances (c'est-à-dire ce qui est le mieux) varient avec la taille de la liste et de la fenêtre. J'aime bien cette version parce qu'elle est en deux lignes (elle pourrait être en une ligne, mais je préfère nommer les concepts).

Il s'avère que le code ci-dessus est mauvais . Cela fonctionne si le paramètre passé à itérable est une séquence mais pas si c'est un itérateur. Si c'est un itérateur, le même itérateur est partagé (mais pas en tee) entre les appels islice et cela casse gravement les choses.

Voici un code corrigé :

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 autre version pour les livres. Au lieu de copier un itérateur puis d'avancer les copies plusieurs fois, cette version fait des copies par paire de chaque itérateur au fur et à mesure que nous avançons la position de départ. Ainsi, l'itérateur t fournit à la fois l'itérateur "complet" avec un 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)

7voto

Gus Points 83

J'utilise le code suivant comme une simple fenêtre coulissante qui utilise des générateurs pour augmenter drastiquement la lisibilité. D'après mon expérience, sa vitesse est jusqu'à présent suffisante pour une utilisation en analyse de séquence bioinformatique.

Je l'inclus ici car je n'ai pas encore vu cette méthode utilisée. Encore une fois, je ne fais aucune déclaration quant à ses performances comparées.

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 votes

Le principal inconvénient ici est le len(sequence) appel. Cela ne fonctionnera pas si sequence est un itérateur ou un générateur. Lorsque l'entrée tient dans la mémoire, cela offre une solution plus lisible que les itérateurs.

0 votes

Oui, vous avez raison. Ce cas particulier a été conçu à l'origine pour analyser des séquences d'ADN qui sont généralement représentées sous forme de chaînes de caractères. Il a certainement la limitation que vous mentionnez. Si vous le souhaitez, vous pouvez simplement tester chaque tranche pour vous assurer qu'elle a toujours la bonne longueur et oublier de connaître la longueur de la séquence entière. Mais cela ajouterait un peu plus de surcharge (un test len() à chaque itération).

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X