Force trial order reshuffle until a constraint is met

OS Ubuntu

PsychoPy version v1.83.04

What are you trying to achieve?: I have a list of words that belong to either category “A” or category “B”. I would like to present them in a pseudo-randomized fashion, constrained to having no more than four words belonging to the same category being presented in a row. I would like to keep this in the Builder, with some additional code which I assume will be necessary to achieve this presentation order.

What did you try to make it work?: I have been working on this problem for a few days now and I kind of lost track of all my attempts, but basically I played around with the loops and conditions files looking for a way to make the program sample either “A” or “B” words according to a predefined order that already met the no-more-than-four-in-a-row criterion.

I created a pseudo-random “abstract order” (i.e., a stimulus category order) that met the constraint, and put it into an outer loop that my main trial loop could refer to. Therefore, I had something like $itemType as input in the “Conditions” field of my trial loop, and also "itemType as a header in my “trialList.csv” file in the outer loop. The “itemType” column in the “trialList.csv” file contained repetitions of either the “listA.csv” or “listB.csv” filenames as trials, thus pointing to separate files containing the words that belong to either the “A” or the “B” category.

My first (and naive) difficulty was that the program would go through either the whole “A” or “B” lists, rather than picking just one word/trial. Because I could not find a way to independently assign a “weight” or “repetition” parameter to specific trials in a list, in Builder mode, I added a “rowSelect” column to the “trialList.csv”, and I used it as a way to select only a specific row on each trials, under the “Selected rows” field of my trial loop.

Now in principle this kind of solved my problem, however in a not-so-elegant way. Indeed, I´d rather randomize which word is presented, because I fear that having both the “abstract order” (i.e., the stimulus condition order) and the actual order of the stimuli (i.e., which word is displayed) pre-specified may introduce excessive order effects in my procedure.

Other users here already suggested a “brute force” approach to similar problems, i.e., shuffling the list, checking it, repeat until the desired constraint is met, then feed it to the loop that presents the trials. This would be a great solution to me, as it would both randomize the “abstract” and the actual order of words, however my limited knowledge of python could use some directions on how to implement this with code snippets prior to the loops that manage trial presentation.

Please be aware that I make no promises about this working - from the limited information you have given about your needs, the following may be adjustable for your study:

# -*- coding: utf-8 -*-
import random

"""
A Brute Force method for pseudorandom list generation by Oliver Clark

In the start experiment tab paste the code below.

Enter your words into wordList1 or 2.  This could probably be accompished in a code component by doing something like
wordList1 = nameOfColumnContainingWords (not tested)
in the start of routine tab.

It then combines both wordsLists into a masterlist.

mixList shuffles words in each list and then compares the position to each individual list to obtain
the order.  It keeps track of the order by storing a string in the orderCounter List.

extractValue pops the last word from each list into the trialList.  This means than no words are repeated.

orderCheck makes sure the same list hasn't been sampled from 4 times in a row.

We then run a for loop to generate the trialList.

To access the words - in a textStim you would have $trialList[thisN] as the text, setting every repeat (not tested).

"""
wordList1 = [1,3,5,7,9,11,13,15,17,19,21] 
wordList2 = [2,4,6,8,10,12,14,16,18,20,22]
allWords = [wordList1,wordList2]

trialList = []
orderCounter = []

def mixList():
    random.shuffle(wordList1)
    random.shuffle(wordList2)
    random.shuffle(allWords)
    if allWords[0] == wordList1:
        orderCounter.append('l1')
    else:
        orderCounter.append('l2')

def extractValue():

    trialList.append(allWords[0].pop())
    trialList.append(allWords[1].pop())


def orderCheck():
    if orderCounter[(len(orderCounter)-4):(len(orderCounter)-1)] == list('l1'*4): #change the integer value if you want fewer repetitions ot be checked for (e.g. [0:1] & 'l1'*1] 
                                          #would result in no consecutive repeats)
        mixList()
    elif orderCounter[(len(orderCounter)-4):(len(orderCounter)-1)]  == list('l2'*4):
        mixList()

for i in range (len(wordList1)):
    mixList()
    orderCheck()
    extractValue()

print trialList #this can be deleted, just for debugging purposes

BW

Oli

2 Likes

That looks promising! Thank you so much for your help, I will now work my way through your code and see if I can adapt it into a solution that fits my experiment. I will hopefully post a working example soon!

–Davide

Hi Oli,

That’s quite mind-bending and I’m still trying to get my head around whether you’ve actually solved this in a deterministic way (i.e. without brute force iterating until a successful combination is achieved).

Doesn’t your extractValue() function always ensure that the word types alternate within pairs, so that the maximum number of successive runs can never exceed two (e.g. L1, L2; L2, L1)?

Yep, I checked the script extensively and that´s what extractValue() does. So orderCheck() doesn´t come into play at all right now.

Also $trialList[thisN] in textStim (with setting every repeat enabled of course) gives out an error (thisN not defined), but that was easily solved by inserting a trial counter and using it as an index in respect to the trialList.

You’re right Michael! It was one of those situations where because more than 4 never occurred I assumed it was working!

This one seems to work fine and is a little less mind bending.
Oli

# -*- coding: utf-8 -*-
import random
"""
Brute Force PseudoRandom Ver 2
O.Clark
Since the list order needs to vary freely so it can repeat the same word list up to 
four times I have gone for a dictionary key approach.  It's not very sexy but it seems
to work (I can only eyeball so many outputs though - no black swans as of yet.

A list of indexes is generated before creating the trials and then converted into a string
so count can be used to see if any instance of 5 of the same trial in a row occur.  If they do - the list is reshuffled
(the brute force element) - if there are no instances of 5 of the same, a trial list is created by
iterating through the list of dictionary keys and popping the values within.
"""

wordList1 = [1,3,5,7,9,11,13,15,17,19,21] 
wordList2 = [2,4,6,8,10,12,14,16,18,20,22]

dictLists = {'l1':wordList1, 'l2':wordList2}
keyList = list(['l1','l2']*len(wordList1))

random.shuffle(wordList1)
random.shuffle(wordList2)

random.shuffle(keyList)
orderTest=False

while orderTest == False:
    for i in range(len(keyList)):
        if ''.join(keyList).count('l1'*5) > 0 or ''.join(keyList).count('l2'*5) >0:
            random.shuffle(keyList)
            break
        else:
            orderTest = True

trialList=[]

for thisKey in keyList:
    trialList.append(dictLists[thisKey].pop())

print trialList #this can be deleted, just for debugging purposes
1 Like