Python Tutorial: Level 6

Created for the OVGU Cognitive Neuroscience Master's course H.3: Projektseminar Fall 2019

Updated August 2020

Taught by Reshanne Reeder

We are already familiar with using keypresses using event.waitKeys(), but this class has a lot more to it than just controlling the sequence of an experiment. We can also use it to store keypresses for response collection. To store a list of keys, simply define "keys":

In [5]:
from psychopy import event, visual, monitors

mon = monitors.Monitor('myMonitor', width=35.56, distance=60)
mon.setSizePix([1920, 1080])
win = visual.Window(monitor=mon, size=(400,400), color=[-1,-1,-1])

my_text = visual.TextStim(win)

nTrials=10

for trial in range(nTrials):
    my_text.text = "Please make a keypress for trial " + str(trial)
    My_text.draw()
    win.flip()
    keys = event.waitKeys()
    print(keys)
    
win.close()    
['1']
['h']
['space']
['tab']
['8']
['return']
['down']
['up']
['b']
['backslash']
183.1865 	WARNING 	Monitor specification not found. Creating a temporary one...

Running this code, you will find that you are only able to make a single keypress for each trial. This is because we have made a keypress a necessary step to move on to the next trial (waitkeys). The experiment waits for any keypress, and as soon as the condition is met, it goes on to the next trial. You can see the name of the key when it is printed. Note that keypresses are always stored as strings.

If you want to make keypresses independent of the trial flow, use event.getKeys:

In [ ]:
from psychopy import core

for trial in range(nTrials):
    core.wait(2)
    keys=event.getKeys()
    print(keys)

win.close()

The difference between event.waitKeys and event.getKeys is that the former will stop the experiment until a key is pressed, whereas the latter is independent of trial flow. In the example above, you can make as many keypresses as you want within 2 seconds, and they will be recorded in a list. The list will be refreshed for each trial.

If you only want certain keypresses to count toward response collection, you can add a keylist as an argument in event.getKeys or .waitKeys (remember to code accepted keys as strings):

In [ ]:
for trial in range(nTrials):
    keys=event.getKeys(keyList=['1','2'])
    #draw some stuff
    #win flip
    #wait
    print(keys)

win.close()

Now only a 1 or 2 response will be recorded in keys. If you use event.waitKeys, only a 1 or 2 response will allow you to go on to a subsequent trial. This is good for controlling against accidental responses. This technique is also a good way for an experimenter to control the start of an experiment. For example:

In [19]:
from psychopy import core, event, visual, monitors

mon = monitors.Monitor('myMonitor', width=35.56, distance=60)
mon.setSizePix([1920, 1080])
win = visual.Window(monitor=mon, size=(400,400), color=[-1,-1,-1])

nTrials=10
my_text=visual.TextStim(win)

#To start the experiment, experimenter presses a "w" (arbitrary)
my_text.text = "Wait for experimenter to start"
my_text.draw()
win.flip()
event.waitKeys(keyList=['w'])

win.close()
2350.8660 	WARNING 	Monitor specification not found. Creating a temporary one...

The window will only close once you've pressed the correct button.

Although getKeys is more flexible than waitKeys, because getKeys records responses across the whole trial, it can be quite hard to control. For example, say you want to present an initial fixation, followed by a stimulus, and you only want to collect responses during stimulus presentation and not during the fixation. You can implement getKeys like this:

In [ ]:
for trial in range(nTrials):
    
    keys = event.getKeys(keyList=['1','2']) #put getkeys HERE??
    my_text.text = "trial %i" %trial #insert integer into the string with %i
    
    fix.draw()
    win.flip()
    core.wait(2)
    
    my_text.draw()
    win.flip()
    core.wait(1)
    
    print(keys) #which keys were pressed?
    
win.close()

But you'll notice that you collect keys across the whole trial. Even if you put getKeys only after fixation, it still collects keys across the whole trial:

In [ ]:
for trial in range(nTrials):
    
    my_text.text = "trial %i" %trial #insert integer into the string with %i
    
    fix.draw()
    win.flip()
    core.wait(2)
    
    keys = event.getKeys(keyList=['1','2']) #put getkeys HERE??
    
    my_text.draw()
    win.flip()
    core.wait(1)
    
    print(keys) #which keys were pressed?
    
win.close()

To give yourself a bit more control with this, you can add "event.clearEvents" immediately preceding the point at which you want to start collecting responses. This function flushes any irrelevant keys that have been pressed and starts the key collection anew:

In [27]:
from psychopy import core, event, visual, monitors

mon = monitors.Monitor('myMonitor', width=35.56, distance=60)
mon.setSizePix([1920, 1080])
win = visual.Window(monitor=mon, size=(400,400), color=[-1,-1,-1])

nTrials=10
my_text=visual.TextStim(win)
fix=visual.TextStim(win, text='+')

for trial in range(nTrials):
    
    keys = event.getKeys(keyList=['1','2']) #put getkeys HERE
    my_text.text = "trial %i" %trial #insert integer into the string with %i
    
    fix.draw()
    win.flip()
    core.wait(2)
    
    event.clearEvents() #clear events HERE
    
    my_text.draw()
    win.flip()
    core.wait(1)
    
    print(keys) #which keys were pressed?
    
win.close()
['2']
[]
[]
[]
['1', '2']
['2', '1']
['1', '2']
['1', '2', '1', '2']
['1']
['1']
4003.7258 	WARNING 	Monitor specification not found. Creating a temporary one...

You can see the empty lists where I tried to respond during fixation.

With "getKeys", you are prone to recording multiple responses as seen above, because it will automatically collect as many responses as you make within the allotted time. So how do you only take the first response a subject makes during a trial (that is, only record the first response as the "true" response, and ignore subsequent responses)? You should add a separate response collector variable in this case:

In [9]:
if keys: #if there are keypresses stored in keys
    sub_resp = keys[0] #only count the first keypress

Implemented in the full script, it looks like this:

In [28]:
from psychopy import core, event, visual, monitors

mon = monitors.Monitor('myMonitor', width=35.56, distance=60)
mon.setSizePix([1920, 1080])
win = visual.Window(monitor=mon, size=(400,400), color=[-1,-1,-1])

nTrials=10
my_text=visual.TextStim(win)
fix=visual.TextStim(win, text='+')

for trial in range(nTrials):
    
    keys = event.getKeys(keyList=['1','2']) #put getkeys HERE
    my_text.text = "trial %i" %trial #insert integer into the string with %i
    
    fix.draw()
    win.flip()
    core.wait(2)
    
    event.clearEvents() #clear events HERE
    
    my_text.draw()
    win.flip()
    core.wait(1)
    
    print("keys that were pressed", keys) #which keys were pressed?
    
    if keys:
        sub_resp = keys[0] #only take first response
        
    print("response that was counted", sub_resp)    
    
win.close()
keys that were pressed ['1']
response that was counted 1
keys that were pressed ['1']
response that was counted 1
keys that were pressed ['1', '2', '1', '2', '1']
response that was counted 1
keys that were pressed ['2', '1', '2', '1', '2']
response that was counted 2
keys that were pressed ['2', '2']
response that was counted 2
keys that were pressed ['1', '2']
response that was counted 1
keys that were pressed ['2']
response that was counted 2
keys that were pressed ['1', '2', '1']
response that was counted 1
keys that were pressed ['1']
response that was counted 1
keys that were pressed ['1']
response that was counted 1
4201.5368 	WARNING 	Monitor specification not found. Creating a temporary one...

getKeys and waitKeys also have a "timeStamped" option, which in principle could be used to record response timing, but personally I have found that this is difficult to control. Instead, there are a couple of different ways of recording responses (the second way we will get to in the psychtoolbox portion of this tutorial). The first way (before the days of PsychoPy3), I recorded responses using clock timing (see level5) and the "maxWait" argument of waitKeys. maxWait allows you to enter an amount of time (in seconds) that a stimulus should appear on screen. For example:

In [ ]:
#waits for 2 seconds, then continues if no response
keys = event.waitKeys(maxWait=2, keyList=['1', '2'])

Then, you can record the exact time during a trial in which a participant made a response, using a clock, implemented like so:

In [29]:
from psychopy import core, event, visual, monitors

mon = monitors.Monitor('myMonitor', width=35.56, distance=60)
mon.setSizePix([1920, 1080])
win = visual.Window(monitor=mon, size=(400,400), color=[-1,-1,-1])

nTrials=10
my_text=visual.TextStim(win)

rt_clock = core.Clock()  # create a response time clock

for trial in range(nTrials):
    rt_clock.reset() #reset timing for every trial
    event.clearEvents(eventType='keyboard') #reset keys for every trial

    my_text.text = "trial %i" % trial
    my_text.draw()
    win.flip()

    keys = event.waitKeys(maxWait=2, keyList=['1', '2']) #waits for 2 seconds then continues
    if keys:
        print(rt_clock.getTime(), keys) #get time at which the subject made a keypress

win.close()
0.8851170539855957 ['1']
0.009994983673095703 ['2']
0.4510948657989502 ['2']
0.39842700958251953 ['1']
0.1160120964050293 ['2']
0.10191988945007324 ['1']
0.12199902534484863 ['2']
0.12213587760925293 ['1']
0.10178208351135254 ['2']
0.18268513679504395 ['1']
4869.3628 	WARNING 	Monitor specification not found. Creating a temporary one...

This code will present the stimulus for maximally 2 seconds, and print the response time for each trial. As it is coded now, the stimulus presentation time is pseudo-response-dependent- that is, the response terminates a trial (if a response is made within 2 seconds), or the experiment continues after 2 seconds(if there is no keypress). This may look a little jarring or messy because stimulus presentation times are all different. So how do you make a stimulus appear for 2 seconds regardless of the response made, but still record response time accurately? One method is to add another (identical) stimulus for the remaining time following a response:

In [ ]:
#waits for stimulus duration then continues
    keys = event.waitKeys(maxWait=2, keyList=['1', '2'])
    if keys:
        resp_time = rt_clock.getTime() #use getTime to determine the response time
        #stimulus duration minus however long it took the subject to respond
        remaining_time=2-resp_time
        my_text.draw()
        win.flip()
        core.wait(remaining_time)

Implemented in a functional snippet like this:

In [30]:
from psychopy import core, event, visual, monitors

mon = monitors.Monitor('myMonitor', width=35.56, distance=60)
mon.setSizePix([1920, 1080])
win = visual.Window(monitor=mon, size=(400,400), color=[-1,-1,-1])

nTrials=10
my_text=visual.TextStim(win)

rt_clock = core.Clock()  # create a response time clock

for trial in range(nTrials):
    rt_clock.reset() #reset timing for every trial
    event.clearEvents(eventType='keyboard') #reset keys for every trial

    my_text.text = "trial %i" % trial
    my_text.draw()
    win.flip()
    
    #waits for 2 seconds then continues
    keys = event.waitKeys(maxWait=2, keyList=['1', '2'])
    if keys:
        resp_time = rt_clock.getTime() #use getTime to determine the response time
        #stimulus duration minus however long it took the subject to respond
        remaining_time=2-resp_time
        my_text.draw()
        win.flip()
        core.wait(remaining_time)

win.close()
5132.0273 	WARNING 	Monitor specification not found. Creating a temporary one...

Now, regardless of how long it takes a subject to respond, the stimulus appears for the full 2 seconds, and response time is recorded at the moment the participant made a response.

If you want to use a countdown timer or frame-based timing, recording responses is a little different. Because the presentation timing is dependent on a while loop (see level5), you will have to use "getKeys" instead of "waitKeys", and instruct your experiment to "listen" for keypresses during the while loop. For example:

In [34]:
from psychopy import core, event, visual, monitors

mon = monitors.Monitor('myMonitor', width=35.56, distance=60)
mon.setSizePix([1920, 1080])
win = visual.Window(monitor=mon, size=(400,400), color=[-1,-1,-1])

nTrials=10
my_text=visual.TextStim(win)

rt_clock = core.Clock()  # create a response time clock
cd_timer = core.CountdownTimer() #add countdown timer

for trial in range(nTrials):
    rt_clock.reset()  # reset timing for every trial
    cd_timer.add(2) #add 2 seconds

    event.clearEvents(eventType='keyboard')  # reset keys for every trial
    while cd_timer.getTime() > 0: #for 2 seconds

        my_text.text = "trial %i" % trial
        my_text.draw()
        win.flip()

        keys = event.getKeys(keyList=['1', '2'])  #collect keypresses after first flip

        if keys:
            resp_time = rt_clock.getTime() #use getTime to determine the response time
            print(keys, resp_time) #print keys and response times

win.close()
['1'] 0.9140980243682861
['2'] 1.3682599067687988
['1'] 0.6841731071472168
['2'] 0.7842271327972412
['1'] 0.8676011562347412
['2'] 0.9844369888305664
['1'] 1.0844440460205078
['1'] 0.7508609294891357
['2'] 0.8343038558959961
['1'] 0.9511518478393555
['2'] 1.051271915435791
['1'] 0.4839608669281006
['2'] 0.5840470790863037
['1'] 0.684161901473999
['2'] 0.7842919826507568
['1'] 0.9010560512542725
['1'] 1.3515918254852295
['2'] 1.4516689777374268
['1'] 1.569648027420044
['2'] 1.651831865310669
['1'] 0.7012319564819336
['2'] 0.8177180290222168
['1'] 0.9182240962982178
['2'] 1.0678980350494385
5729.2688 	WARNING 	Monitor specification not found. Creating a temporary one...

This allows you to get rid of core.wait(), and also the repeated stimulus presentation for any "remaining time" following a response.

However, experiment timing with the CountdownTimer recruits that tricky "getKeys" function again for response collection, and if you test out your snippet (as shown above), you'll see you've got the problem of collecting multiple keys. You cannot simply add a "sub_resp" variable because the keys are collected independently for every frame now. So how do you only take the first response in this case? I use "count" to count up the number of responses made on a given trial:

In [ ]:
    count=-1 #reset the counter for every while loop
    while cd_timer.getTime() > 0: #for 2 seconds

        my_text.text = "trial %i" % trial
        my_text.draw()
        win.flip()

        keys = event.getKeys(keyList=['1', '2'])  #collect keypresses after first flip
        
        if keys:
            count=count+1 #count up the number of times a key is pressed
            
            if count == 0: #if this is the first time a key is pressed
                resp_time = rt_clock.getTime()
                sub_resp = keys      

Implemented in a functioning code snippet, it looks like this:

In [36]:
from psychopy import core, event, visual, monitors

mon = monitors.Monitor('myMonitor', width=35.56, distance=60)
mon.setSizePix([1920, 1080])
win = visual.Window(monitor=mon, size=(400,400), color=[-1,-1,-1])

nTrials=10
my_text=visual.TextStim(win)

rt_clock = core.Clock()  # create a response time clock
cd_timer = core.CountdownTimer() #add countdown timer

for trial in range(nTrials):
    rt_clock.reset()  # reset timing for every trial
    cd_timer.add(2) #add 2 seconds

    event.clearEvents(eventType='keyboard')  # reset keys for every trial
    
    count = -1 #start the counter for the while loop
    
    while cd_timer.getTime() > 0: #for 2 seconds

        my_text.text = "trial %i" % trial
        my_text.draw()
        win.flip()

        keys = event.getKeys(keyList=['1', '2'])  #collect keypresses after first flip

        if keys:
            count=count+1 #count up the number of times a key is pressed
            
            if count == 0: #if this is the first time a key is pressed
                resp_time = rt_clock.getTime() #get RT for first response in that loop
                sub_resp = keys #get key for only the first response in that loop
                
    print(sub_resp, resp_time)

win.close()
['1'] 0.763812780380249
['1'] 0.8175280094146729
['2'] 1.0013179779052734
['2'] 1.3681600093841553
['2'] 1.0178210735321045
['2'] 1.0176191329956055
['2'] 0.7507009506225586
['2'] 0.6865460872650146
['2'] 1.2180027961730957
['2'] 0.90087890625
6315.7569 	WARNING 	Monitor specification not found. Creating a temporary one...

Now you only collect one response per trial.

Finally, what if you need to exit in the middle of the experiment? It is good to have an escape route in case a subject needs to stop early, or there is any other unforeseen problem. In this case, you can tell your experiment to do different things depending on which button is pressed:

In [ ]:
keys = event.getKeys(keyList=['1', '2', 'escape'])  #collect keypresses after first flip

        if keys:
            if 'escape' in keys: #if someone wants to escape the experiment
                win.close() #close the window
            else: #otherwise...
                resp_time = rt_clock.getTime()
                print(resp_time, keys)

It is important to adjust the output of your experiment in a way that is easiest for you to read it, so you can catch errors and easily interpret stored results. This brings us to how to record data so that it can be saved easily. There are many ways to record data across an experiment, so it's up to you how you want to organize it all. I collect data in lists or dictionaries to save to different file formats. Let's start with lists. First, I pre-define lists of zeros that will be filled online:

In [25]:
nTrials=3
nBlocks=2

sub_resp = [[0]*nTrials]*nBlocks

print(sub_resp)
[[0, 0, 0], [0, 0, 0]]

In the above example with sub_resp, I have created 2 lists of zeros (one for each block), each with a size of 3 (number of trials). These lists will then be filled during response collection:

In [ ]:
sub_resp = [[0]*nTrials]*nBlocks

for block in range(nBlocks):
    #...
    for trial in range(ntrials):
        #...
        
        if keys:
            if 'escape' in keys:
                core.quit()
            else:
                if key == 0:
                    sub_resp[block][trial] = key

Using indexing, you can fill the relevant keys that are pressed and the response time for every trial. If you exit the experiment early, any keypresses that have been made up until that point will be stored, but everything else will remain zeros.

As for recording accuracy, the correct response for a given trial will depend on your particular task. As an easy example, say you want your participants to do a simple math problem on each trial (sometimes used as an attention check for central visual focus):

In [19]:
math_problems = ['1+3=','1+1=','3-2=','4-1='] #write a list of simple arithmetic
solutions = [4,2,1,3] #write solutions
prob_sol = list(zip(math_problems,solutions))

print(prob_sol)
[('1+3=', 4), ('1+1=', 2), ('3-2=', 1), ('4-1=', 3)]

If you randomly select a problem from the list on a trial-by-trial basis, you can update the "prob" (the problem that will be shown on that trial) and the "corr_resp" (the correct response for that trial), in a trial-by-trial manner before presenting the stimuli:

In [29]:
import numpy as np

nBlocks=2
nTrials=4

corr_resp = [[0]*nTrials]*nBlocks
prob = [[0]*nTrials]*nBlocks

for block in range(nBlocks):
    
    for trial in range(nTrials):
        #choose a random problem from the list
        prob[block][trial] = prob_sol[np.random.choice(4)]
        #the solution is at index 1 in the zipped list
        corr_resp[block][trial] = prob[block][trial][1]
        
        print(prob[block][trial], corr_resp[block][trial])
        
        #draw stimulus here
('1+3=', 4) 4
('1+3=', 4) 4
('3-2=', 1) 1
('4-1=', 3) 3
('1+3=', 4) 4
('1+1=', 2) 2
('4-1=', 3) 3
('3-2=', 1) 1

Then, after the stimulus is presented (the math problem), to record subject accuracy, you compare the subject's response to the correct response:

In [ ]:
        #record subject accuracy
        #correct- remembers keys are saved as strings
        if sub_resp[block][trial] == str(corr_resp[block][trial]):
            sub_acc[block][trial] = 1 #arbitrary number for accurate response
        #incorrect- remember keys are saved as strings              
        elif sub_resp[block][trial] != str(corr_resp[block][trial]):
            sub_acc[block][trial] = 2 #arbitrary number for inaccurate response 
                                    #(should be something other than 0 to distinguish 
                                    #from non-responses)

All together, in a functional snippet, this looks like:

In [33]:
from psychopy import core, event, visual, monitors

#monitor specs
mon = monitors.Monitor('myMonitor', width=35.56, distance=60)
mon.setSizePix([1920, 1080])
win = visual.Window(monitor=mon, size=(400,400), color=[-1,-1,-1])

#blocks, trials, stims, and clocks
nBlocks=2
nTrials=4
my_text=visual.TextStim(win)
rt_clock = core.Clock()  # create a response time clock
cd_timer = core.CountdownTimer() #add countdown timer

#prefill lists for responses
sub_resp = [[0]*nTrials]*nBlocks
sub_acc = [[0]*nTrials]*nBlocks
prob = [[0]*nTrials]*nBlocks
corr_resp = [[0]*nTrials]*nBlocks

#create problems and solutions to show
math_problems = ['1+3=','1+1=','3-2=','4-1='] #write a list of simple arithmetic
solutions = [4,2,1,3] #write solutions
prob_sol = list(zip(math_problems,solutions))

for block in range(nBlocks):
    for trial in range(nTrials):
        #what problem will be shown and what is the correct response?
        prob[block][trial] = prob_sol[np.random.choice(4)]
        corr_resp[block][trial] = prob[block][trial][1]
        
        rt_clock.reset()  # reset timing for every trial
        cd_timer.add(3) #add 3 seconds

        event.clearEvents(eventType='keyboard')  # reset keys for every trial
        
        count=-1 #for counting keys
        while cd_timer.getTime() > 0: #for 3 seconds

            my_text.text = prob[block][trial][0] #present the problem for that trial
            my_text.draw()
            win.flip()

            #collect keypresses after first flip
            keys = event.getKeys(keyList=['1','2','3','4','escape'])

            if keys:
                count=count+1 #count up the number of times a key is pressed

                if count == 0: #if this is the first time a key is pressed
                    #get RT for first response in that loop
                    resp_time[block][trial] = rt_clock.getTime()
                    #get key for only the first response in that loop
                    sub_resp[block][trial] = keys[0] #remove from list

        #record subject accuracy
        #correct- remembers keys are saved as strings
        if sub_resp[block][trial] == str(corr_resp[block][trial]):
            sub_acc[block][trial] = 1 #arbitrary number for accurate response
        #incorrect- remember keys are saved as strings              
        elif sub_resp[block][trial] != str(corr_resp[block][trial]):
            sub_acc[block][trial] = 2 #arbitrary number for inaccurate response 
                                    #(should be something other than 0 to distinguish 
                                    #from non-responses)
                    
        #print results
        print('problem=', prob[block][trial], 'correct response=', 
              corr_resp[block][trial], 'subject response=',sub_resp[block][trial], 
              'subject accuracy=',sub_acc[block][trial])

win.close()
problem= ('1+1=', 2) correct response= 2 subject response= 2 subject accuracy= 1
problem= ('4-1=', 3) correct response= 3 subject response= 3 subject accuracy= 1
problem= ('1+1=', 2) correct response= 2 subject response= 1 subject accuracy= 2
problem= ('1+3=', 4) correct response= 4 subject response= 4 subject accuracy= 1
problem= ('1+1=', 2) correct response= 2 subject response= 2 subject accuracy= 1
problem= ('3-2=', 1) correct response= 1 subject response= 1 subject accuracy= 1
problem= ('1+3=', 4) correct response= 4 subject response= 3 subject accuracy= 2
problem= ('4-1=', 3) correct response= 3 subject response= 3 subject accuracy= 1
1513.0258 	WARNING 	Monitor specification not found. Creating a temporary one...

At the end of the experiment, you should have complete lists of everything you would like to save in a file:

In [34]:
print(prob)
print(corr_resp)
print(sub_resp)
print(sub_acc)
[[('1+1=', 2), ('3-2=', 1), ('1+3=', 4), ('4-1=', 3)], [('1+1=', 2), ('3-2=', 1), ('1+3=', 4), ('4-1=', 3)]]
[[2, 1, 4, 3], [2, 1, 4, 3]]
[['2', '1', '3', '3'], ['2', '1', '3', '3']]
[[1, 1, 2, 1], [1, 1, 2, 1]]

The filename for your data should be the filename you have defined at the beginning of your script (which we went over many moons ago in level4). The classic (but least flexible) way of saving data is using a comma-separated value (csv) file. To save these lists in a csv, you need to 1.) import the csv module, 2.) provide the filename and directory name, 3.) data to be saved, and 4.) save location:

In [40]:
import csv #1

#2 create filename and directory
filename = 'savecsv_example.csv'
print(filename)

import os
main_dir = os.getcwd() #define the main directory where experiment info is stored
#point to a data directory to save the output
data_dir = os.path.join(main_dir,'exp','data',filename)
print(data_dir)

#3: a list of lists of all the data you want to save
data_as_list = [prob, corr_resp, sub_resp, sub_acc]
#print(data_as_list)

#4: mode='w' means 'write mode'. "sub_data" is arbitrary, but stay consistent
with open(data_dir, mode='w') as sub_data:
    #delimiter=',' for lists of values separated by commas
    data_writer = csv.writer(sub_data, delimiter=',')
    data_writer.writerow(data_as_list) #write
savecsv_example.csv
/home/shoarly/Documents/pytutorial/exp/data/savecsv_example.csv

This will give you a file that looks like this:

"[[('1+1=', 2), ('3-2=', 1), ('1+3=', 4), ('4-1=', 3)], [('1+1=', 2), ('3-2=', 1), ('1+3=', 4), ('4-1=', 3)]]","[[2, 1, 4, 3], [2, 1, 4, 3]]","[['2', '1', '3', '3'], ['2', '1', '3', '3']]","[[1, 1, 2, 1], [1, 1, 2, 1]]"

This looks pretty ugly, and csv is pretty bare-bones in terms of aesthetics. You don't need to save all your experiment data in one file, of course. If you want, you can separate files by block or by type (probs, corr_resp, etc.). You can specify this when you are creating your csv files. For example:

In [45]:
filename = 'savecsv_example2' #leave off the extension for now, since saving multiple files
data_dir = os.path.join(main_dir,'exp','data',filename)
#to save each data type individually with one block per row
data_as_list = [prob, corr_resp, sub_resp, sub_acc]

#add a types list for labelling
types = ['problem','correct_answer','subject_response','subject_accuracy']

count=-1 #add a counter to cycle through filenames

for data in data_as_list: #open each data type to save individually
    count=count+1
    
    #add type to filename
    with open(data_dir + '_' + types[count] + '.csv', mode='w') as sub_data:
        data_writer = csv.writer(sub_data, delimiter=',')
        for block in data: #loop over each block now
            data_writer.writerow(block) #write a new row for that block

If done correctly, this should create 4 files (one for each data type), with 2 rows in each file (one for each block), with 4 data points per row (one for each trial).

Another method of saving your data is the JSON file. JSON requires that you save your data as a dictionary. You can append data to dictionaries using an online method like I did with the lists, or you could transform your lists into dictionaries if you would rather stick with lists for online data collection. The latter is shown here:

In [65]:
for block in range(nBlocks):
    #run experiment
    #...
    #save data
    data_as_dict = []
    for a,b,c,d in zip(prob[block], corr_resp[block], sub_resp[block], sub_acc[block]):
        #the names listed here do not need to be the samr as the variable names
        data_as_dict.append({'problem':a,'corr_resp':b,'sub_resp':c,'sub_acc':d})
        
    print(data_as_dict)        
[{'problem': ('1+1=', 2), 'corr_resp': 2, 'sub_resp': '2', 'sub_acc': 1}, {'problem': ('3-2=', 1), 'corr_resp': 1, 'sub_resp': '1', 'sub_acc': 1}, {'problem': ('1+3=', 4), 'corr_resp': 4, 'sub_resp': '3', 'sub_acc': 2}, {'problem': ('4-1=', 3), 'corr_resp': 3, 'sub_resp': '3', 'sub_acc': 1}]

Then, you would use the following syntax to save the data to JSON:

In [63]:
import json as json

for block in range(nBlocks):
    #run experiment
    #...

    #JSON files can be saved with txt or JSON extension, I like to use .txt
    filename = 'savejson_example'
    data_dir = os.path.join(main_dir,'exp','data',filename)

    data_as_dict = []
    for a,b,c,d in zip(prob[block], corr_resp[block], sub_resp[block], sub_acc[block]):
        #the names listed here do not need to be the samr as the variable names
        data_as_dict.append({'problem':a,'corr_resp':b,'sub_resp':c,'sub_acc':d})
        
    with open(data_dir + '_block%i.txt'%block, 'w') as outfile:
        json.dump(data_as_dict, outfile)

And you get output that looks something like this for each block:

[{"problem": ["1+1=", 2], "corr_resp": 2, "sub_resp": "2", "sub_acc": 1}, {"problem": ["3-2=", 1], "corr_resp": 1, "sub_resp": "1", "sub_acc": 1}, {"problem": ["1+3=", 4], "corr_resp": 4, "sub_resp": "3", "sub_acc": 2}, {"problem": ["4-1=", 3], "corr_resp": 3, "sub_resp": "3", "sub_acc": 1}]

That was a lot less scripting to get where you wanted, wasn't it? But just like learning how to calculate ANOVAs by hand in year 1 stats, learning how to save CSVs is a lesson in appreciating all the hard work that went into your ancestors' python.

So why save your data in a dictionary in the first place? Saving your data as lists makes it easier to copy/paste saved data into excel, for example. But my opinion is that loading data in excel is rather messy and limited -- you are prone to lose bits of data here and there just by human error, and you can't do a lot of analyses in excel, either. Saving your data as a dictionary makes it much nicer to read and analyze in python. So for the final part of the tutorial, I will demonstrate how to load saved data from a dictionary using the JSON example.

First, importing a dictionary back into python in a readable, tabular format, is as easy as using the "pandas" module (the python data analysis library). First, import pandas:

In [51]:
import pandas as pd #shorten name for ease of reference

When you then type "pd.", you can press tab and scroll through all the various options pandas contains. One of these is "read_json":

In [67]:
#load the imported data as a variable (df)
df = pd.read_json(data_dir+'_block1.txt')
print(df)
     problem  corr_resp  sub_resp  sub_acc
0  [1+1=, 2]          2         2        1
1  [3-2=, 1]          1         1        1
2  [1+3=, 4]          4         3        2
3  [4-1=, 3]          3         3        1

Then, your data are loaded as a "DataFrame" object (df), which you can view in tabular format. Doesn't that look nice??

Because your data load as an object, you can specify which columns to print using the df.X method:

In [68]:
print(df.problem)
0    [1+1=, 2]
1    [3-2=, 1]
2    [1+3=, 4]
3    [4-1=, 3]
Name: problem, dtype: object

You can also print your data as a formatted table using the pd.DataFrame function (the aesthetics of results vary depending on the environment you are in):

In [55]:
pd.DataFrame(df)
Out[55]:
problem corr_resp sub_resp sub_acc
0 [1+1=, 2] 2 2 1
1 [3-2=, 1] 1 1 1
2 [1+3=, 4] 4 3 2
3 [4-1=, 3] 3 3 1

You can also filter your data in different ways:

In [85]:
acc_trials = df.loc[df['sub_acc'] == 1] #show only trials on which subject was correct
print(acc_trials)
     problem  corr_resp  sub_resp  sub_acc
0  [1+1=, 2]          2         2        1
1  [3-2=, 1]          1         1        1
3  [4-1=, 3]          3         3        1

You can also calculate mean accuracy this way:

In [86]:
len(acc_trials)/len(df['sub_resp']) #divide 1 responses by total responses
Out[86]:
0.75

You can also compute correlations between columns of data:

In [77]:
print("Pearson r:")
print(pd.DataFrame.corr(df,method='pearson'))
print("Spearman rho:")
print(pd.DataFrame.corr(df,method='spearman'))
Pearson r:
           corr_resp  sub_resp   sub_acc
corr_resp   1.000000  0.943880  0.774597
sub_resp    0.943880  1.000000  0.522233
sub_acc     0.774597  0.522233  1.000000
Spearman rho:
           corr_resp  sub_resp   sub_acc
corr_resp   1.000000  0.948683  0.774597
sub_resp    0.948683  1.000000  0.544331
sub_acc     0.774597  0.544331  1.000000