Reaction time
Measure the delay between a stimulus onset event and the participant's next key press. The canonical psychophysics measurement in a LABO experience.
What this recipe does
For each stimulus-onset row in EventPoints.csv, find the first KeyDown in InputCapture.csv that occurred afterward and compute the time difference using MonotonicExecutionTime. Output is one reaction-time value per stimulus.
Files you need
EventPoints.csv— provides stimulus-onset timing.InputCapture.csv— provides key-press timing.ExperienceState.csv— optional, only if you need per-frame context (fps during the press, epoch name) attached to each reaction-time value.
Code
- Python (pandas)
- R
import pandas as pd
events = pd.read_csv("EventPoints.csv")
inputs = pd.read_csv("InputCapture.csv")
# 1. Filter to stimulus onsets.
# Adjust the filter to match how your experience tags stimulus events.
stim_onsets = events[
(events["Event_Type"] == "Response")
& (events["Event_Description"] == "Stimulus flash")
].sort_values("MonotonicExecutionTime").reset_index(drop=True)
# 2. Filter to key-down events for the response key(s).
response_keys = {"Space", "Return"}
key_downs = inputs[
(inputs["eventType"] == "KeyDown")
& (inputs["keyCode"].isin(response_keys))
].sort_values("MonotonicExecutionTime").reset_index(drop=True)
# 3. For each stimulus, find the first KeyDown strictly after it.
pairs = pd.merge_asof(
stim_onsets,
key_downs,
on="MonotonicExecutionTime",
direction="forward",
suffixes=("_stim", "_key"),
allow_exact_matches=False,
)
# 4. Reaction time in seconds.
pairs["reaction_time_s"] = pairs["MonotonicExecutionTime_key"] - pairs["MonotonicExecutionTime"]
# 5. Drop trials with no response (NaN in _key columns).
valid = pairs.dropna(subset=["MonotonicExecutionTime_key"])
print(valid[["EpochName_stim", "Event_Description", "keyCode", "reaction_time_s"]].describe())
library(dplyr)
library(data.table)
events <- fread("EventPoints.csv")
inputs <- fread("InputCapture.csv")
stim_onsets <- events %>%
filter(Event_Type == "Response", Event_Description == "Stimulus flash") %>%
arrange(MonotonicExecutionTime)
key_downs <- inputs %>%
filter(eventType == "KeyDown", keyCode %in% c("Space", "Return")) %>%
arrange(MonotonicExecutionTime)
# Rolling join: for each stimulus, find the first KeyDown >= stim time.
setDT(stim_onsets); setDT(key_downs)
stim_onsets[, join_t := MonotonicExecutionTime]
key_downs[, join_t := MonotonicExecutionTime]
paired <- key_downs[stim_onsets, on = .(join_t), roll = -Inf]
paired[, reaction_time_s := join_t - i.join_t]
valid <- paired[!is.na(reaction_time_s)]
summary(valid$reaction_time_s)
Gotchas
- Match your stimulus filter to your experience. The filter
Event_Description == "Stimulus flash"is an example — use whatever description your EventPoint was configured with. If unsure,head(events)and read theEvent_Descriptionvalues. direction="forward", not"nearest". You want the first response after the stimulus, not the closest in either direction. Using"nearest"can pair a stimulus with a key press that happened before it.allow_exact_matches=Falseis deliberate — if the sameMonotonicExecutionTimesomehow lands on both a stimulus and a key press (vanishingly unlikely but possible), treat them as independent.- Trials with no response show as
NaNin the merged row. Drop them explicitly rather than silently including them — aNaNreaction time is meaningful data (miss). - CPU time vs. display time. The stimulus
MonotonicExecutionTimeis when the CPU recorded the event. The participant actually saw the stimulus some milliseconds later — see Timing → Display-time correction. For high-precision RT, add the display offset to stimulus times before subtracting. - Response-key mismatch. Double-check
response_keysagainst the keys configured on yourKeyCodeDataCaptureSO. If the participant's keys aren't in the capture config, noKeyDownrows exist for them.