How to change a sliding window generator to jump two elements at a time?

Viewed 111

I've reproduced the sliding window code shown here, but I need to modify it to jump two elements at a time instead of just one.

Original code:

def window(seq, n=3):
    it= iter(seq)
    result = list(islice(it, n))
    if len(result) == n:
        yield result
    for elem in it:
        result = result[1:] + [elem,]
        yield result

If I start with the following list:

My_List= ['adl_01_11', 'adl_01_12', 'adl_01_13', 'adl_01_14', 'adl_02_15', 'adl_02_16', 'adl_02_17', 'adl_02_18', 'adl_02_19', 'adl_02_20', 'adl_02_21', 'adl_02_22']

and I apply the window over My_List, I get the following result:

[['adl_01_11', 'adl_01_12', 'adl_01_13'], ['adl_01_12', 'adl_01_13', 'adl_01_14'], ['adl_01_13', 'adl_01_14', 'adl_02_15'], ['adl_01_14', 'adl_02_15', 'adl_02_16'], ['adl_02_15', 'adl_02_16', 'adl_02_17'], ['adl_02_16', 'adl_02_17', 'adl_02_18'], ['adl_02_17', 'adl_02_18', 'adl_02_19'], ['adl_02_18', 'adl_02_19', 'adl_02_20'], ['adl_02_19', 'adl_02_20', 'adl_02_21'], ['adl_02_20', 'adl_02_21', 'adl_02_22']]

How do I change this function if I want to iterate through 2 items at a time? That means I expect a result like this:

[['adl_01_11', 'adl_01_12', 'adl_01_13'], ['adl_01_13', 'adl_01_14', 'adl_01_15'], ['adl_01_15', 'adl_01_16', 'adl_02_17'], ['adl_01_17', 'adl_02_18', 'adl_02_19'], ['adl_02_19', 'adl_02_20', 'adl_02_21']]

Notice that adl_02_22 is no longer in the results, and my window iterates every 2 items.

In the window function I tried changing result[1:] to result[2:] but it doesn't work well. Any idea?

3 Answers

I propose three solutions for this problem:

  1. a specific one for a window of size 3 with a step of 2,
  2. a general one with any window and step size,
  3. skip all this and use existing libraries.

Solution 1: sliding window with hard-coded size=3 and step=2

If you replace the for elem in it: loop by the equivalent while True: loop which tries next(it) until StopIteration is raised, that will let you use next(it) twice per iteration instead of just once:

def window_size3_step2(seq):
    it = iter(seq)

    try:
        result = [0,0,next(it)]
    except StopIteration:
        return

    while True:
        try:
            result = [result[2], next(it), next(it)]
        except StopIteration:
            break
        else:
            yield result


My_List= ['adl_01_11', 'adl_01_12', 'adl_01_13', 'adl_01_14', 'adl_02_15', 'adl_02_16', 'adl_02_17', 'adl_02_18', 'adl_02_19', 'adl_02_20', 'adl_02_21', 'adl_02_22']

print(f"{list(window_size3_step2(My_List))}")

Output:

[['adl_01_11', 'adl_01_12', 'adl_01_13'], ['adl_01_13', 'adl_01_14', 'adl_02_15'], ['adl_02_15', 'adl_02_16', 'adl_02_17'], ['adl_02_17', 'adl_02_18', 'adl_02_19'], ['adl_02_19', 'adl_02_20', 'adl_02_21']]

Testing for shorter lists:

for n in range(7):
    print(f"len={n} result={list(window_size3_step2(range(n)))}")

len=0 result=[]
len=1 result=[]
len=2 result=[]
len=3 result=[[0, 1, 2]]
len=4 result=[[0, 1, 2]]
len=5 result=[[0, 1, 2], [2, 3, 4]]
len=6 result=[[0, 1, 2], [2, 3, 4]]

Solution 2: general window function with arbitrary size and step

This second solution goes back to using islice to take into account the given window size argument, which I've renamed size for clarify, and accepts a step argument that can also take any positive integer value.

from itertools import islice
def window(seq, size=3, step=1):
    if size < 1 or step < 1:
        raise ValueError("Nobody likes infinite loops.")
    it = iter(seq)
    result = list(islice(it, size))
    while len(result) == size:
        yield result
        if step >= size:
            result = list(islice(it, step-size, step))
        else:
            result = result[step:] + list(islice(it, step))

On your input list, window(My_List, size=3, step=2), or just window(My_List, step=2), returns the list of lists you want.

I've also tested this with a wide variety of seq length, size and step, and I can confirm it works correctly in all cases. E.g., the output of this loop (try it yourself, I don't want to paste this long output here) is correct on every line:

for input_size in range(10):
    for window_size in range(1,4):
        for step_size in range(1,4):
            print(f"len={input_size} size={window_size} step={step_size} "
                  f"result={list(window(range(input_size), size=window_size, step=step_size))}")

Solution 3: there's a library for this!

The more_itertools library already provides a function doing just this:

I had to install it first:

pip3 install more_itertools

Use it:

from more_itertools import windowed
print(f"{list(windowed(My_List, 3, step=2))}")

[('adl_01_11', 'adl_01_12', 'adl_01_13'), ('adl_01_13', 'adl_01_14', 'adl_02_15'), ('adl_02_15', 'adl_02_16', 'adl_02_17'), ('adl_02_17', 'adl_02_18', 'adl_02_19'), ('adl_02_19', 'adl_02_20', 'adl_02_21'), ('adl_02_21', 'adl_02_22', None)]

It's not exactly what you asked for, though, because it pads the last incomplete window with None (or any fill value you provide) instead of truncating the end.

While using existing libraries is often a good choice, I learned more creating solutions 1 and 2, and I hope you find value in the progression.

Credits:

I found the more_itertools solution here: https://stackoverflow.com/a/46412374/3216427

I think you may have transcribed your expected output incorrectly.

The items in your expected output, (adl_01_15, adl_01_16, adl_01_17), do not exist in My_List.

If so, this will do:

islice(window(My_List), 0, None, 2)

and if you don't need a generator:

list(window(My_List))[::2]
# input
lst = ['adl_01_11', 'adl_01_12', 'adl_01_13', 'adl_01_14', 'adl_02_15', 'adl_02_16', 'adl_02_17', 'adl_02_18', 'adl_02_19', 'adl_02_20', 'adl_02_21', 'adl_02_22']
# remove last entry if required
lst = lst[:-(len(lst[1:]) % 2)]
# get midpoints of sublist and add previous and following value to it
lst = [[lst[i-1], x, lst[i+1]] for i, x in enumerate(lst) if ((i+1) % 2) == 0]

print(lst)
# [['adl_01_11', 'adl_01_12', 'adl_01_13'],
#  ['adl_01_13', 'adl_01_14', 'adl_02_15'],
#  ['adl_02_15', 'adl_02_16', 'adl_02_17'],
#  ['adl_02_17', 'adl_02_18', 'adl_02_19'],
#  ['adl_02_19', 'adl_02_20', 'adl_02_21']]
Related