web-dev-qa-db-fra.com

Itérateur de fenêtre roulante ou coulissante?

J'ai besoin d'une fenêtre roulante (ou 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. Quelqu'un a-t-il une méthode plus pythonique, moins verbeuse ou plus efficace pour le faire?

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]
"""
121
David B.

Il y en a un dans une ancienne version de la documentation Python avec itertools examples :

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 des docs est un peu plus succinct et utilise itertools avec plus d’effet, j’imagine.

101
Daniel DiPaolo

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

Voici une implémentation optimisée basée sur deque basée sur 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 facilement tout le reste affiché ici la plupart du temps, bien que la version tee de pillmuncher le bat pour les grands itérables et les petites fenêtres. Sur les plus grandes fenêtres, la deque avance à nouveau à la vitesse brute.

L'accès aux éléments individuels dans la variable deque peut être plus rapide ou plus lent qu'avec les listes ou les nuplets. (Les éléments proches du début sont plus rapides ou ceux 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 déque (itération d’un élément à l’autre est rapide, cette boucle a donc fonctionné 20% plus vite que la méthode la plus rapide suivante, la méthode pillmuncher). Lorsque je l'ai modifiée pour rechercher individuellement et ajouter des éléments dans une fenêtre de dix, les tables tournaient 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 de l'addition, mais tee était encore un peu plus rapide. Dans l’ensemble, j’estimerais que l’un ou l’autre est très 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.

44
kindall

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]
30
pillmuncher

Voici une généralisation qui ajoute le support 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 en roulant step positions par itération, en complétant 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 obtenir un exemple de cas d'utilisation du paramètre step, voir Traitement efficace d'un fichier .txt volumineux en python .

16
jfs

Juste une contribution rapide.

Comme les documents Python actuels ne comportent pas de "fenêtre" dans les exemples itertool (c'est-à-dire, au bas de http://docs.python.org/library/itertools.html ), voici un extrait basé sur le code pour le groupeur 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 en avant. Ensuite, nous les compressons ensemble. Notez que cette fonction renvoie un générateur (ce n'est pas directement un générateur lui-même).

Tout comme pour les versions d’ajout d’élément et d’avant-itérateur décrites ci-dessus, les performances (c’est-à-dire la meilleure solution) varient en fonction de la taille de la liste et de la taille de la fenêtre. J'aime celui-ci parce qu'il s'agit d'une doublure (ce pourrait être une doublure, mais je préfère nommer les concepts).

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

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)

En outre, une version de plus pour les livres. Au lieu de copier un itérateur, puis d'avancer plusieurs fois, cette version effectue des copies par paires de chaque itérateur à mesure que nous avancons la position de départ. Ainsi, l'itérateur t fournit à la fois l'itérateur "complet" avec le point de départ en t et également 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)
9
MrDrFenner

J'utilise le code suivant comme une simple fenêtre glissante qui utilise des générateurs pour améliorer considérablement la lisibilité. Sa rapidité a jusqu'ici été suffisante pour une utilisation en analyse de séquence bioinformatique, selon mon expérience.

Je l’inclus ici parce que je n’ai pas encore vu cette méthode utilisée. Encore une fois, je n’ai aucune prétention à propos de 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]
6
Gus

une version légèrement modifiée de la fenêtre deque, pour en faire une véritable fenêtre évolutive. Pour qu'il commence à être peuplé avec un seul élément, puis qu'il atteigne la taille maximale de la fenêtre, puis qu'il diminue, Edge le reste à 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))

cela donne

[0]
[0, 1]
[0, 1, 2]
[1, 2, 3]
[2, 3, 4]
[3, 4]
[4]
5
Dmitry Avtonomov
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
heyyou482

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)]
3
Nikolay Frick

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 lorsque la séquence est terminée, et pour une bonne raison qui me dépasse, l'instruction de rendement ici l'excepte et la fonction retourne, en 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 linéaire mais dont la seule exigence est que seq implémente soit __iter__ ou __getitem__ et ne repose pas sur itertools ou collections à part la solution de @ dansalmo :)

2
jameh

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é dans Python doc . Vous pouvez facilement l’étendre à une fenêtre plus large. 

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

Fait cela pour une fonction moyenne glissante

2
yazdmich

Rendons-le paresseux!

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
Gram
#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]

"" "

1
FAYAZ

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
keocra
>>> 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
dansalmo

Modifié Réponse de DiPaolo pour permettre un remplissage arbitraire et une taille de pas variable

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
shouldsee

C'est une vieille question, mais pour ceux qui sont toujours intéressés, il existe une excellente implémentation d'un curseur de fenêtre utilisant des générateurs dans this page (par Adrian Rosebrock). 

C'est une implémentation pour OpenCV, mais vous pouvez facilement l'utiliser à d'autres fins. Pour les plus enthousiastes, je collerai le code ici, mais pour mieux le comprendre, je vous recommande de visiter la page d'origine. 

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

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

À votre santé

0
DarkCygnus