Choosing subet of items to present

PennController for IBEX Forums Support Choosing subet of items to present

This topic contains 27 replies, has 5 voices, and was last updated by Avatar Maria Evjen 5 days, 15 hours ago.

Viewing 15 posts - 1 through 15 (of 28 total)
  • Author
    Posts
  • #4890
    Avatar
    mg5171
    Participant

    Hi,
    I’m wondering if there’s a way to choose a random subset of items to present, and to mix in fillers and stim items.

    1. Random subset of items: I’d like to present all of the items labeled ‘stim’, but a random subset of 50 labeled ‘filler’. I currently have two blocks, one of which presents all of the ‘stim’ items, and another which presents all of the ‘filler’ items, like this:

    Template( 
        PennController.defaultTable.filter("type", "stim"), //choose only stimuli items
        
        variable => 
    
    newTrial("experiment",

    I have an analogous one for ‘filler’ items–but I want to tell it to choose a random subset of e.g. 50, instead of presenting all of them.

    2. If I can manage that, then I have 2 blocks, one with stim and one with fillers. I know about the randomize and shuffle commands that go with Sequence() at the beginning. Is there some combination of those I can use to randomize both stim and fillers, and then present them all in a random order?

    Thanks,
    Maddie

    • This topic was modified 7 months, 2 weeks ago by Avatar mg5171.
    #4892
    Jeremy
    Jeremy
    Keymaster

    Hi Maddie

    1. I can see a couple ways to do this, but you’ll need some plain javascript anyway. Here’s a suggestion, assuming you have an Item column in your table that goes from 0 to, say, 100 for your filler items:

    numberRows = 100;
    numberDraws = 50;
    randomIDs = [...new Array(numberRows)].map((v,i)=>i).sort(v=>Math.random()>0.5).splice(0,numberDraws);
    
    Template( defaultTable.filter( r => r.type=="filler" && randomIDs.indexOf(Number(r.Item))>-1 ),
      variable => newTrial(
        // ...
    

    2. I think what you are looking for is anyOf, you can use it like this: Sequence( "intro" , randomize(anyOf("experiment","filler")) )

    #4896
    Avatar
    mg5171
    Participant

    Thanks so much Jeremy!

    #5144
    Avatar
    mg5171
    Participant

    To follow up on this, I have the following. The items have a column called ‘number’ and are labeled 1-90. This seems to be giving me the first 45 only, but in a randomized order (which is also probably because I’m randomizing with the Sequence command at the beginning. Any ideas why it’s giving me the first 45 instead of a random 45 out of the total 90?

    //fillers
    
    numberRows_fillers = 90; 
    numberDraws_fillers = 45; 
    randomIDs_fillers = [...new Array(numberRows_fillers)].map((v,i)=>i).sort(v=>Math.random()>0.5).splice(1,numberDraws_fillers); 
    
    Template( defaultTable.filter( r => r.type=="filler" && randomIDs_fillers.indexOf(Number(r.number))>-1 ),
        variable =>
    
    #5146
    Jeremy
    Jeremy
    Keymaster

    Just to double-check: your table defines 90 filler items whose ‘number’ label goes from 1 to 90, right?

    When I test it I do get items whose number is over 45, but they indeed seem massively underrepresented. It seems to improve a bit when changing the > comparison when randomly sorting the indices for a >= comparison

    Here’s my full script if you want to compare with yours:

    PennController.ResetPrefix(null)
    
    let myTable = "type,number,text\n"
    
    for (let i = 0; i < 90; i++)
        myTable += "filler,"+Number(i+1)+",textfiller"+i+"\ntest,"+Number(i+1)+",texttest"+i+"\n";
    
    AddTable("myTable", myTable)
    
    Sequence( randomize(anyOf(startsWith("test"),startsWith("filler"))) )
    
    numberRows_fillers = 90; 
    numberDraws_fillers = 45; 
    randomIDs_fillers = [...new Array(numberRows_fillers)].map((v,i)=>i).sort(v=>Math.random()>=0.5).splice(1,numberDraws_fillers); 
    
    Template( GetTable("myTable").filter( r => r.type=="filler" && randomIDs_fillers.indexOf(Number(r.number))>-1 ),
        row => newTrial( "filler-"+row.number , newText("filler").print(),newButton(row.text).print().wait() )
    )
    
    Template( GetTable("myTable").filter( r => r.type=="test" && randomIDs_fillers.indexOf(Number(r.number))>-1 ),
        row => newTrial( "test-"+row.number , newText("test").print(),newButton(row.text).print().wait() )
    )

    Jeremy

    #5852
    Avatar
    samsanto
    Participant

    Hi Jeremy,

    Jumping in here because this is pretty similar to something I am trying to accomplish.

    I have an experiment with 12 blocks but the stimuli for each block are randomly generated from a master list (csv) for each participant. Essentially, one block consists of displaying 12 random “train” words and 12 random “test” words from this master list. Right now I have a file with three columns (Item, Type, Word) where item is just an index, type is Train v. Test, and Word is the actual stimulus.

    I need to generate 24 random subsets of my master list- so 12 of the “Train” words, and 12 “Test” words- without replacement (the main issue) so I guarantee that each item is only seen once, and cycle through them to display the words.

    I appreciate your help and any thoughts you have!

    Thanks,
    Sam

    #5853
    Jeremy
    Jeremy
    Keymaster

    Hi Sam,

    There probably are more optimal implementations, but how about this:

    const NBLOCKS = 24;
    const NITEMSPERBLOCK = 12;
    
    const blocks = {};
    const addItem = type => {
        if (!blocks.hasOwnProperty(type))
            blocks[type] = [...new Array(NBLOCKS)].map((v,i)=>Object({id:i, n:0}));
        let freeblocks = blocks[type].filter(v=>v.n<NITEMSPERBLOCK);
        if (!freeblocks.length) return console.error("# of items in table does not match config");
        let r = Math.floor(Math.random()*freeblocks.length);
        freeblocks[r].n++;
        return type+"-"+freeblocks[r].id;
    }
    
    Sequence(
        rshuffle("Test-0","Train-0"),"pause",
        rshuffle("Test-1","Train-1"),"pause",
        rshuffle("Test-2","Train-2"),"pause",
        rshuffle("Test-3","Train-3"),"pause",
        rshuffle("Test-4","Train-4"),"pause",
        rshuffle("Test-5","Train-5"),"pause",
        rshuffle("Test-6","Train-6"),"pause",
        rshuffle("Test-7","Train-7"),"pause",
        rshuffle("Test-8","Train-8"),"pause",
        rshuffle("Test-9","Train-9"),"pause",
        rshuffle("Test-10","Train-10"),"pause",
        rshuffle("Test-11","Train-11"),"pause",
        rshuffle("Test-12","Train-12"),"pause",
        rshuffle("Test-13","Train-13"),"pause",
        rshuffle("Test-14","Train-14"),"pause",
        rshuffle("Test-15","Train-15"),"pause",
        rshuffle("Test-16","Train-16"),"pause",
        rshuffle("Test-17","Train-17"),"pause",
        rshuffle("Test-18","Train-18"),"pause",
        rshuffle("Test-19","Train-19"),"pause",
        rshuffle("Test-20","Train-20"),"pause",
        rshuffle("Test-21","Train-21"),"pause",
        rshuffle("Test-22","Train-22"),"pause",
        rshuffle("Test-23","Train-23")
    )
    
    newTrial("pause", newButton("Take a break").print().wait() )
    
    Template( "myTable" , row =>
        newTrial( addItem(row.Type) ,
            newButton(row.Word).print().wait()
        )
    )

    This will randomly assign each of the items from your table to one of 24 (NBLOCKS) type-specific blocks of 12 (NITEMSPERBLOCK) items each. You can then write a Sequence where you randomly shuffle the items in those randomly-generated blocks, and separate each block by a pause trial.

    Jeremy

    #5862
    Avatar
    samsanto
    Participant

    Hi Jeremy,

    Thanks so much! This works pretty much how I need, I did have to switch around a couple things. Mainly I want all the “Train” to go first in a block then all the “Test” so I took out the rshuffle(…) but that was easy.

    Now I’m having a problem where I want to do slightly different key presses depending on if its a “train” or “test.” For the train words, I just display the word, for the test words, I ask want to get user input. Based on how we set it up, each block is (hopefully) guaranteed to have 12 words in it so I am keeping a global counter variable that just switches off every 12 words the action (see below). However, this isn’t working. The counter is not actually incrementing and it is stuck on the “failure” option, any idea why?

    Also if you have a better way to implement this, let me know (I feel like its too “hardcoded” if you know what I mean…)
    Thank you for the continued help, I really appreciate it!!

    
    PennController.Sequence(
        "Learn-0","Test-0","pause",
        "Learn-1","Test-1","pause",
        "Learn-2","Test-2","pause",
        "Learn-3","Test-3","pause",
        "Learn-4","Test-4","pause",
        "Learn-5","Test-5","pause",
        "Learn-6","Test-6","pause",
        "Learn-7","Test-7","pause",
        "Learn-8","Test-8","pause",
        "Learn-9","Test-9","pause",
        "Learn-10","Test-10","pause",
        "Learn-11","Test-11","pause"
    );
    PennController.ResetPrefix(null);
    
    newVar("count")
        .set(0)
        .global()
    ;
    
    PennController("pause", newButton("Take a break").print().wait() );
    
    PennController.Template( "statTableRandom.csv" , row =>
        newTrial( addItem(row.Type) ,
        
            getVar("count").test.is(12)
                .success(
                    getVar("count").set(0)
                    ,
                    newText(row.Word).print()
                    ,
                    newKey("yn").wait()
                )
                .failure(
                    getVar("count").set(v=>v+1)
                    ,
                    newText(row.Word).print()
                    ,
                    newKey(" ").wait()
                )
        )
    );
    

    -Sam

    #5863
    Jeremy
    Jeremy
    Keymaster

    Hi Sam,

    PennController elements are not meant to be used outside a newTrial command. Just check the value of the Type column to pass the appropriate parameter to your newKey command:

    Template( "statTableRandom.csv" , row =>
      newTrial( addItem(row.Type) ,
        newText(row.Word).print()
        ,
        newKey( (row.Type=="Train"?" ":"yn") ).wait()  
      )
    )

    Jeremy

    #5864
    Avatar
    samsanto
    Participant

    Oh wow, duh! Forgot I had that… for that exact point.

    Thanks so much!!

    #6197
    Avatar
    Maria Evjen
    Participant

    Hi!

    This is similar to what I am trying to do as well, and the script you posted for choosing a subset of items has really helped me a lot! I have a few issues though:

    1. With your script I have managed to choose a subset of items from my target and filler stimuli, but I have the same issue as Maddie that when I choose 29 out of 44 items, I’m only getting the 29 first items in my table. I tried to implement your comment with the >= comparison, but I am still only getting the first 29 items. Is there any way to solve this?

    2. I have three categories of stimuli: alternating targets, non-alternating targets, and fillers. In the training part of my experiment, participants will be shown 29 out of 44 alternating targets, 17 out of 22 non-alternating targets, and all 18 fillers. Except for the issue in 1, I have this part solved.

    In the testing part of the experiment, I want to choose 30 alternating targets, 10 non-alternating targets and 10 fillers. Crucially, for both the alternating and the non-alternating targets, I want half of the items in the testing phase to be items that were already shown in training and the other half to be the remaining 15 (for alternating targets) and 5 (for non-alternating targets) items in the table. Do you know if there is any way to implement this?

    I’m quite new to programming and have not used javascript before, so any help is much appreciated!

    Thanks,
    Maria

    #6198
    Jeremy
    Jeremy
    Keymaster

    Hi Maria,

    Since my last reply on this thread, I came up with another pretty useful function, pick, that I introduced on that thread. It makes it easier to randomize a set of trials and pick just a subset of it, and maybe even pick some more from the remaining trials later on if your design requires it (which your current design does require, if I understand correctly). You also no longer have to use GetTable().filter or define those number variables.

    Your current design has one more requirement though, which is to retrieve the trials that you previously picked, and from that retrieval set, pick again a random subset of trials. So I will propose a new version of the pick function here, which will allow you to embed pick recursively (since you need to pick from a picked set). To be clear, you should use the code below, not the one from the linked thread—you can place it at the top of your main script for example:

    function Pick(set,n) {
        assert(set instanceof Object, "First argument of pick cannot be a plain string" );
        n = Number(n);
        if (isNaN(n) || n<0) 
            n = 0;
        this.args = [set];
        this.runSet = null;
        set.remainingSet = null;
        this.run = arrays => {
            if (this.runSet!==null) return this.runSet;
            const newArray = [];
            if (set.remainingSet===null) {
                if (set.runSet instanceof Array) set.remainingSet = [...set.runSet];
                else set.remainingSet = arrays[0];
            }
            for (let i = 0; i < n && set.remainingSet.length; i++)
                newArray.push( set.remainingSet.shift() );
            this.runSet = [...newArray];
            return newArray;
        }
    }
    function pick(set, n) { return new Pick(set,n); }

    Then you can use it like this, for example:

    fillers = randomize("filler")
    tests = randomize("test")
    
    picked_fillers = pick(fillers, 4)
    picked_tests = pick(tests, 3)
    
    repeat_fillers = pick(randomize(picked_fillers), 2)
    repeat_tests = pick(randomize(picked_tests), 2)
    
    Sequence(
        picked_fillers/*4 trials*/, picked_tests/*3 trials*/,
        "break",
        repeat_fillers/*2 trials*/ , repeat_tests/*2 trials*/ , pick(fillers,1)/*1 trials*/ , pick(tests,2)/*2 trials*/
    )

    Some explanations:

    1. I first randomize all the trials labeled “filler” and all the trials labeled “test”, and I store the result in variables that I name fillers and tests, respectively
    2. Then I use pick on those variables, so as to pick the first four and the first three trials from those randomized sets, and store the picked trials in picked_fillers and picked_tests, respectively. After that, the sets stored in the variables fillers and tests have lost four and three trials, respectively
    3. Then I use pick on the (randomized) variables picked_fillers and picked_tests, and store the result in repeat_fillers and repeat_tests, respectively. You need to randomize here, because otherwise you would always pick the first trials from the sets in the picked_ variables, which would effectively result in systematically repeating the oldest (=first) trials from the first half of the experiment
    4. Finally, I build my Sequence using those variables, and also use pick one last time on fillers and tests again which, at that point, no longer contain the trials that were transferred to picked_fillers and picked_tests in the meantime, so we’re sure that by doing so, we’re not repeating trials that were already presented in the first half

    If you need further randomization, you can use rshuffle (e.g. rshufflle(repeat_fillers,repeat_tests,pick(fillers,1),pick(tests,2))) or randomize, but keep in mind that the latter only accepts single arguments, so you would need to also use seq in this case: randomize( seq(repeat_fillers,repeat_tests,pick(fillers,1),pick(tests,2)) )

    Let me know if you have questions

    Jeremy

    #6199
    Avatar
    Maria Evjen
    Participant

    Hi Jeremy,

    Thank you so much for your reply! This is exactly what I want to do, and I’ve managed to implement most of it in my script. I just have one more question: in the training part of the experiment, I have three templates with trials labelled “alt”, “non”, and “fil” (corresponding to your “test” and “filler”) and I have made the following variables:

    alt = randomize(“alt”)
    non = randomize(“non”)
    fil = randomize(“fil”)

    picked_alt = pick(alt, 29)
    picked_non = pick(non, 17)
    picked_fil = pick(fil, 18)

    repeat_alt = pick(randomize(picked_alt), 15)
    repeat_non = pick(randomize(picked_non), 5)
    repeat_fil = pick(randomize(picked_fil), 10)

    I then use rshuffle on the picked_alt, picked_non, and picked_fil variables in my Sequence. This part of the experiment works perfectly!

    My issue is that my testing trials have a different template than my training trials. Parallel to the training phase, my testing phase contains three templates, one for each of “alt”, “non”, and “fil”, but if I label these using the same names as in the training phase, my test trials are getting mixed in with my training trials. But if I label them something else, e.g. “test_alt” etc., I don’t know how to implement these trials in the variables above and still make sure that I can pick the repeat items and the remaining items in my table.

    Thanks in advance!

    Maria

    #6200
    Jeremy
    Jeremy
    Keymaster

    Hi,

    Do I understand you correctly that you have rows in your table from which you generate both training and test trials, so that each row is associated with two trials (one training, one test) and that you want to show some training trials in the training phase, and in the test phase, you want to select half of those training trials but show the corresponding test trial instead (ie. the one that was generated from the same row, but using a different template) and then show the test trials corresponding to the training trials that were not shown in the training phase?

    If that’s what you want to do, the problem with the current solution is that you generate separate training and test trials (even if generated from the same rows) so that the trials picked by the pick function will either be the training or the test ones, and so applying pick again later will not exclude the dual trials of the type that was not picked before.

    From here, you have two options: either you generate a single trial for each row, whose content can dynamically switch between training or test depending on as part of which phase you’re running it, or you give up on the pick function and manually use arrays for the specific needs of your design. The latter option requires good knowledge of javascript and how (PC)Ibex generates and orders its trials though.

    If you decide to go with the former option (if feasible at all with your design), you could do something like this:

    Sequence( "intro", "trials", "transition", "trials" )
    
    AddTable("table", "Text\nhello\nworld\nbye\ntown")
    
    newTrial("intro", newVar("phase", "training").global() )
    newTrial("transition", getVar("phase").set("test") )
    
    const test_trial = row => [
      newText("test "+row.Text).print()
      ,
      newButton("Next").print().wait()
    ]
    
    const training_trial = row => [
      newText("training "+row.Text).print()
      ,
      newButton("Next").print().wait()
    ]
    
    
    Template( "table" , row =>
      newTrial( "trials" ,
        getVar("phase").test.is("training")
            .success( ...test_trial(row) )
            .failure( ...training_trial(row) ) 
      )
      .log("phase", getVar("phase") )
    )

    This way, you have a single label (trials in my example) for the two versions of each item, and you can use pick as intended (not illustrated above)

    Let me know if you have questions

    Jeremy

    #6202
    Avatar
    Maria Evjen
    Participant

    Hi again,

    Yes, I am generating both the training trials and the test trials from the same rows in my table as I am using some of the same audio files and images in both training and testing. The way you describe the design above is correct!

    Since I don’t have good knowledge of javascript, I am opting for the former option you suggest, but I am having trouble implementing it in my script. More specifically, where does the implementation of the pick function and the sequence you suggested in your earlier reply fit into this script?

    Sorry if this is a very basic question! And thank you for your continued help, I really appreciate it!

    Maria

Viewing 15 posts - 1 through 15 (of 28 total)

You must be logged in to reply to this topic.