Record keypress and time

I would like to output a results file for my experiment which contains the keys being pressed and the timepoints of those events (preferably as a Pandas Dataframe which I can save to csv).

It seems to me TrialHandler might be what I am looking for, but the documentation doesn’t seem to address saving keypress events, nor do the few examples I could find address any use cases similar to mine.
Can you help me out?

Basically I would be looking for something like MyMagicObject.add([t, resp_key[0]).
Here’s a non-self-contained code example of what I think things should look like:

while pre_evaluation or routineTimer.getTime() > 0:

	resp_key = event.getKeys(keyList=keylist)
	if pre_evaluation:
		if "return" in resp_key:
			routineTimer.reset() 
			routineTimer.add(trial_duration)
			pre_evaluation=False
	else:
		pass

	# get current time
	t = trialClock.getTime()
	if resp_key:
		if resp_key[0] in keys_of_interest:
			MyMagicObject.add([t, resp_key[0])
	win.flip()

If I’m understanding the issue correctly, it’s much easier than you think, and unless you have a particular reason for doing so you won’t have to deal with Pandas at all. If I’m missing something feel free to let me know.

One of the nice things about python is that it doesn’t care about the type of information you put in a list (array), even slightly. ints, floats, strings, whatever, it all goes in the box. So, for your ‘magic object’, you can just use a list and the ‘append’ function. For example, my usual solution would be, somewhere up at the start of the code:

responses=[]

Then, where it now says ‘MyMagicObject.add’ in the code you posted:

tempArray = [t, resp_key[0]]
responses.append(tempArray)

Then you can directly output your responses array to a csv file by adding to your imports:

from psychopy import csv

and then later:

outputWriter1 = csv.writer(open('fileName.csv','w'), lineterminator ='\n') 
for i in range(0, len(responses)):
    outputWriter1.writerow(responses[i])

I usually like to start these sorts of things with a line of headers, which is just a list of strings.

Another note, when you call ‘getKeys’, you can also set the option ‘timeStamped = True’, which will mean that the output of getKeys will be a two-dimensional array with the key identity and the time at which it was pressed. So resp_key[0][0] will be the key itself, and resp_key[0][1] will be the time at which it was pressed. You’ll need to line that up with your trial clock, but it will give you the exact time of the keypress rather than the time when getKeys is called (i.e., the time is recorded in the keypress buffer). It’s a difference of milliseconds, usually, but it does give you a little more precision.

2 Likes

Hi Horea, your “Magic Object” already exists and is indeed the TrialHandler.

At any point, you can add values to be saved in the data file that it will produce, like this:

# specify a column name and cell value for the data file for this particular trial:
yourTrialHandlerName.addData('resp_key', resp_key[0])
yourTrialHandlerName.addData('resp_rt', t)

You can also bundle a number of TrialHandler objects together under an overall ExperimentHandler object, which collates all of their data into a single output data file. In that case, it is better to use the addData() method of the ExperimentHandler object itself, rather than keeping track of which TrialHandler object is current.

I think data file saving is automatic when the ExperimentHandler object closes and doesn’t need to be done explicitly by you (including automatically naming files to avoid overwriting existing data).

Perhaps you could look under the Coder demo menu exp control -> experimentHandler.py and trialHandler.py.

Cheers,

Michael

@jonathan.kominsky
The reason why I was looking for a magic object was that I hoped it would have some added functionality like smart naming, checking for duplicates, etc upon saving. I know I can do this independently of psychopy. It might actually be the best choice, if it turns out TrialHandler is not usable for my purposes.

@m-macaskill
Yeah, I looked at that, but the example seems to be doing something very much unlike what I want. I do not have a stimList, nor do I have trials - but it seems they are mandatory arguments for TrialHandler. I tried to put in some dummy values, but the df it should print for me as per this example ends up being “-1”:

trials = data.TrialHandler([{"a":"a"}], 1)
trials.data.addDataType('event')  # this will help store things with the stimuli
trials.data.addDataType('time')  # add as many types as you like

while pre_evaluation or routineTimer.getTime() > 0:

	# get current time
	t = trialClock.getTime()

	resp_key = event.getKeys(keyList=keylist)
	if pre_evaluation:
		if "return" in resp_key:
			routineTimer.reset()  # clock
			routineTimer.add(trial_duration)
			pre_evaluation=False
	elif resp_key:
		if resp_key[0] in experiment_keylist:
			trials.addData('event', resp_key[0])  # add the data to our set
			trials.addData('time', t)
			# trials.data.add('event', resp_key[0])  # add the data to our set
			# trials.data.add('time', t)
	win.flip()

a = trials.saveAsWideText("testDataWide.txt")

The above code is not self-contained, the full code is here.

Any Idea what’s wrong with my usage of the object?

You can explicitly set the trialList to be None or [], according to the code comment: “user wants an empty trialList which corresponds to a list with a single empty entry”.

But as the name implies, the TrialHandler is designed to handle data from a series of trials which get iterated over. In your example, you keep adding data without advancing to the next entry in the TrialHandler. That means each value will just overwrite the previous one in the first (and only) line of data.

Why you’re not getting any output at all, I don’t know, but if your experiment isn’t structured on a trials basis, then I guess these in-built classes aren’t much use to you anyway. Not sure what the actual procedure is though, so can’t give much advice.

1 Like

As Mike says, Stimlist (the argument is called trialsList) can be [] if you don’t have conditions that vary and you can set the nReps to be 1 in which case your experiment will be a single “trial”.

The reason that you’re not getting any output is that you haven’t advanced to the first trial. This is normally done by a loop using for thisTrial in trials: but in your case you could just call trials.next() after you initialise.

Auto-saving (including filename collision tests) is best handled by ExperimentHandler so I’d recommend you create one and add your trials to it

For a more efficient trial handler there’s a new TrialHandler2 and that might suit you better for this unusual use case (it doesn’t try to precreate space in advance for variables but does so as needed by trial repeats).

So that set of points gives us this working demo:

from psychopy import data

# create trials and advance to 'trial 1'
trials = data.TrialHandler2(trialList=[], nReps=1)
trials.next()  # get trials started
# add to an experiment for auto-saving of files
exp = data.ExperimentHandler(dataFileName = 'data/someExpFilename')
exp.addLoop(trials)

trials.addData('keys',['a','n','c'])
print trials.data
# no need to save file manually; if python exits then it will auto-save
# but you could do exp.saveAsWideText() to be on safe side

You can also save additional info about the experiment above (using extraInfo dictionary).

Now, one last thing is that you’re trying to store multiple key presses and times right now they’re all going into a single ‘cell’ in your data file. You might want to pretend that these are multiple ‘trials’ instead so you get one response per row? To do that definitely use TrialHandler2, set the nReps to be large (many more than the expected number of key presses) and after each response do:

trials.next()
exp.nextEntry()```

Hope that gets you going!
1 Like