0

I have the following list:

item_list = [1, 2, 3, 4, 5]

I want to compare each item in the list to the other items to generate comparison pairs, such that the same comparisons (x, y) and (y, x) are not repeated (i.e. I don't want both [1, 5] and [5, 1]). For the 5 items in the list, this would generate a total of 10 comparison pairs (n*(n-1)/2). I also want to randomize the order of the pairs such that both x- and y-values aren't the same as the adjacent x- and y-values.

For example, this is fine:

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

But not this:

[1, 5]
[1, 4] <-- here the x-value is the same as the previous x-value
[2, 4] <-- here the y-value is the same as the previous y-value
[5, 3]
...

I have only been able to come up with a method in which I manually create the pairs by zipping two lists together (example below), but this is obviously very time-consuming (and would be even more so if I wanted to increase the list of items to 10, which would generate 45 pairs). I also can't randomize the order each time, otherwise I could get repetitions of the same x- or y-values.

x_list = [1, 4, 1, 3, 1, 4, 1, 2, 5, 3] 
y_list = [2, 5, 3, 5, 4, 2, 5, 3, 2, 4] 
zip_list = zip(x_list, y_list)
paired_list = list(zip_list) 
print(paired_list)

I hope that makes sense. I am very new to coding, so any help would be much appreciated!

Edit: For context, my experiment involves displaying two images next to each other on the screen. I have a total of 5 images (labeled 1-5), hence the 5-item list. For each image pair, the participant must select one of the two images, which is why I don't want the same image displayed at the same time (e.g. [1, 1]), and I don't need the same pair repeated (e.g. [1, 5] and [5, 1]). I also want to make sure that each time the screen displays a new pair of images, both images, in their respective positions on the screen, change. So it doesn't matter if an image repeats in the sequence, so as long as it changes position (e.g. [4, 3] followed by [5, 4] is ok).

helen24
  • 3
  • 2
  • Adding this now since someone might toss this in but that's not really random. Can you share the usecase? That might help someone answer better – IanQ Jan 21 '21 at 18:32
  • Thanks @IanQuah, I've edited the post to include the context of my experiment so hopefully that makes things clearer! – helen24 Jan 21 '21 at 19:12
  • 2
    The _easy_ way to maintain randomness when you need to provide constraints is to reroll after getting an unacceptable value (ignore the value you received and just pick another one). – Charles Duffy Jan 21 '21 at 19:13
  • BTW, just so I'm clear -- it's okay for values to be repeated as long as they don't match the value in the same position in the _immediately prior_ item? – Charles Duffy Jan 21 '21 at 19:14
  • @CharlesDuffy that's correct, yes :) – helen24 Jan 21 '21 at 20:36

3 Answers3

1

I had to look up how to generate combinations and random because I have not used them so often, but you should be looking for something like the following:

from itertools import combinations
from random import shuffle

item_list = range(1, 6) # [1, 2, 3, 4, 5]
paired_list = list(combinations(item_list, 2))
shuffle(paired_list)
print(paired_list)
carrvo
  • 511
  • 5
  • 11
1

carrvo's answer is good, but doesn't guarantee the requirement that each iteration-step causes the x-value to change and the y-value to change.

(I'm also not a fan of mutability, shuffling in place, but in some contexts it's more performant)

I haven't thought of an elegant, concise implementation, but I do see a slightly clever trick: Because each pair appears only once, we're already guaranteed to have either x or y change, so if we see a pair for which they don't both change, we can just swap them.

I haven't tested this.

from itertools import combinations
from random import sample  # not cryptographic secure.

def human_random_pairs(items):
    n = len(items)
    random_pairs = sample(combinations(items, 2),
                          n * (n - 1) / 2)
    def generator():
        old = random_pairs[0]
        yield old
        for new in random_pairs[1:]:
            collision = old[0] == new[0] or old[1] == new[1]  # or you can use any, a comprehension, and zip; your choice.
            old = tuple(reversed(new)) if collision else new
            yield old

    return tuple(generator())

This wraps the output in a tuple; you can use a list if you like, or depending on your usage you can probably unwrap the inner function and just yield directly from human_random_pairs, in which case it will "return" the iterable/generator.

Oh, actually we can use itertools.accumulate:

from itertools import accumulate, combinations, starmap
from operator import eq
from random import sample  # not cryptographic secure.

def human_random_pairs(items):
    n = len(items)
    def maybe_flip_second(fst, snd):
        return tuple(reversed(snd)) if any(starmap(eq, zip(fst, snd))) else snd
    return tuple(  # this outer wrapper is optional
        accumulate(sample(combinations(items, 2), n * (n - 1) / 2),  # len(combinations) = n! / r! / (n-r)!
                   maybe_flip_second)
    )
ShapeOfMatter
  • 991
  • 6
  • 25
  • Thank you @ShapeOfMatter! I for some reason couldn't get the code to work unfortunately, and not familiar enough with coding to work out where the issue was, but the ideas were really helpful and helped me find a solution (albeit not very elegant or concise) – helen24 Jan 25 '21 at 17:50
0

Thank you for the contributions! I'm posting the solution I ended up using below for anyone who might be interested, which uses carrvo's code for generating random comparisons and the pair reversal idea from ShapeOfMatter. Overall does not look very elegant and can likely be simplified, but at least generates the desired output.

from itertools import combinations
import random

# Create image pair comparisons and randomize order
no_of_images = 5
image_list = range(1, no_of_images+1) 
pairs_list = list(combinations(image_list, 2))
random.shuffle(pairs_list)
print(pairs_list)

# Create new comparisons sequence with no x- or y-value repeats, by reversing pairs that clash
trial_list = []
trial_list.append(pairs_list[0]) # append first image pair
binary_list = [0] # check if preceding pairs have been reversed or not (0 = not reversed, 1 = reversed)

# For subsequent pairs, if x- or y-values are repeated, reverse the pair
for i in range(len(pairs_list)-1):
    
    # if previous pair was reversed, check against new pair
    if binary_list[i] == 1:
        if trial_list[i][0] == pairs_list[i+1][0] or trial_list[i][1] == pairs_list[i+1][1]:
            trial_list.append(tuple(list(reversed(pairs_list[i+1])))) # if x- or y-value repeats, reverse pair 
            binary_list.append(1) # flag reversal
        else:
            trial_list.append(pairs_list[i+1])
            binary_list.append(0) 
            
    # if previous pair was not reversed, check against old pair            
    elif binary_list[i] == 0:
        if pairs_list[i][0] == pairs_list[i+1][0] or pairs_list[i][1] == pairs_list[i+1][1]:
            trial_list.append(tuple(list(reversed(pairs_list[i+1])))) # if x- or y-value repeats, reverse pair 
            binary_list.append(1) # flag reversal
    
        else:
            trial_list.append(pairs_list[i+1])
            binary_list.append(0)

print(trial_list)
helen24
  • 3
  • 2