Computer Programming
For Psychologists

Experimental design patterns: Implementing practice and experimental trials


Introduction

In a typical experiment, you want to make your participant familiar with the task they are about to perform. This can be done by adding a few practice trials at the start of your experiment. Since these are practice trials, it is not uncommon that these trials include participant feedback as well. In terms of code, your practice trials will obviously look very similar to your experimental trials.

A first approach to implement this procedure might therefore be to have two blocks of code: one in which you present your practice trials and another one in which you present your experimental trials. This is the approach that we will show first below. One major disadvantage of this approach is that whenever you want to make changes to the procedure of your experimental trials, you also have to apply the same changes to your training trials. 

In the second approach, we will show that by organizing your parameters in an efficient manner, you can use a single block of code that presents both your training and experimental trials. 

General setup

In this example experiment we will implement the Posner cueing task. To focus on the organization of the code in practice versus experimental trials, we will leave out a few complexities with regard to how the parameters are generated such as the 80% vs 20% probability of valid and invalid cues, inter stimulus intervals, ... . We start of by importing the relevant modules

#############################
# import modules
#############################
from psychopy import visual, event, core
import random

We will use the following PsychoPy components in the experiments:

#############################
# Set up PsychoPy components
#############################
win = visual.Window(units = 'pix')
cue_left = visual.TextStim(win, "<<")
cue_right = visual.TextStim(win, ">>")
target    = visual.TextStim(win, "x")
feedback  = visual.TextStim(win, "")

A first approach

We start of by creating parameters for both practice as well as test trials. In this simplified version both type of trials will have the full combination of cue direction (left or right) and target location (again left and right). The data for each trial is specified in a dictionary, and these trial dictionaries are added to lists for training trials and experimental trials respectively:

#############################
# Set up parameters
#############################
CUE_DURATION = .4
LEFT         = (-100, 0)
RIGHT        = (0, 100)

training_parameters = []
for cue in ["left", "right"]:
    for stimulus in ["left", "right"]:
        training_parameters.append({
            'cue' : cue,
            'stimulus' : stimulus
            })
random.shuffle(training_parameters)

experimental_parameters = []
for cue in ["left", "right"]:
    for stimulus in ["left", "right"]:
        experimental_parameters.append({
            'cue' : cue,
            'stimulus' : stimulus
            })
random.shuffle(experimental_parameters)

We can now iterate over each of these lists to present the actual trials as follows:

#############################
# Display training trials
#############################
for trial in training_parameters:
    # Present cue
    if trial['cue'] == 'left':
        cue_left.draw()
    else:
        cue_right.draw()
    win.flip()
    core.wait(CUE_DURATION)
    
    # Present stimulus
    if trial['stimulus'] == 'left':
        target.pos = LEFT
    else: 
        target.pos = RIGHT
    target.draw()
    win.flip()
    
    # Wait for response
    response = event.waitKeys(keyList = ["left", "right"])
    
    # Provide feedback
    if response[0] == trial['stimulus']:
        feedback.text = "correct"
    else:
        feedback.text = "incorrect"
            
    feedback.draw()
    win.flip()
    event.waitKeys()

#############################
# Display experimental trials
#############################
for trial in experimental_parameters:
    # Present cue
    if trial['cue'] == 'left':
        cue_left.draw()
    else:
        cue_right.draw()
    win.flip()
    core.wait(CUE_DURATION)
    
    # Present stimulus
    if trial['stimulus'] == 'left':
        target.pos = LEFT
    else: 
        target.pos = RIGHT
    target.draw()
    win.flip()
    
    # Wait for response
    response = event.waitKeys(keyList = ["left", "right"])

Issues with the initial approach

As you can see, there is a lot of duplicated code here. The main difference between the two loops is the addition of feedback when practice trials are presented. Any changes we make to the procedure of the experimental trials (e.g., including an inter stimulus interval), will also have to be added to the procedure that presents training trials. This is of course trivial to do in this simple example, but with a more complex procedure it will require a lot more attention to detail to make sure that both loops remain properly synchronized.

An improved approach

To improve this code, we can start by adding more information to the trial parameters. Since we know when we are generating practice and experimental trials, we will add this phase as an extra field to the dictionary. We will also combine the two separate lists into a single trial parameters list:

#############################
# Set up parameters
#############################
CUE_DURATION = .4
LEFT         = (-100, 0)
RIGHT        = (0, 100)

training_parameters = []
for cue in ["left", "right"]:
    for stimulus in ["left", "right"]:
        training_parameters.append({
            'phase': 'training',
            'cue' : cue,
            'stimulus' : stimulus
            })
random.shuffle(training_parameters)

experimental_parameters = []
for cue in ["left", "right"]:
    for stimulus in ["left", "right"]:
        experimental_parameters.append({
            'phase' : 'test',
            'cue' : cue,
            'stimulus' : stimulus
            })
random.shuffle(experimental_parameters)

trial_parameters = training_parameters + experimental_parameters

We now have a single list with the parameters for both practice and experimental trials in a single list. This means we can use a single loop to present all the trials. And because the phase has been added to the trial dictionary, this can be used to decide whether feedback should be displayed or not:

#############################
# Display trials
#############################
for trial in trial_parameters:
    # Present cue
    if trial['cue'] == 'left':
        cue_left.draw()
    else:
        cue_right.draw()
    win.flip()
    core.wait(CUE_DURATION)
    
    # Present stimulus
    if trial['stimulus'] == 'left':
        target.pos = LEFT
    else: 
        target.pos = RIGHT
    target.draw()
    win.flip()
    
    # Wait for response
    response = event.waitKeys(keyList = ["left", "right"])
    
    # Provide feedback
    if trial['phase'] == 'test':
        if response[0] == trial['stimulus']:
            feedback.text = "correct"
        else:
            feedback.text = "incorrect"
                
        feedback.draw()
        win.flip()
        event.waitKeys()