Working directly with psydat files

The recommended way to analyse your data in Python is to use the provided pandas DataFrame of data and statistics.

However, if you need to you can access the raw data from which these DataFrames are constructed directly as shown in the examples below.

[1]:
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from psychopy.misc import fromFile
from shapely.geometry import LineString
from shapely.geometry import Polygon
from shapely.ops import polygonize
from shapely.ops import unary_union
/home/docs/checkouts/readthedocs.org/user_builds/vstt/envs/stable/lib/python3.11/site-packages/psychopy/preferences/preferences.py:11: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81.
  from pkg_resources import parse_version

Import

A psydat file can be imported using the psychopy fromFile function:

[2]:
psydata = fromFile("example.psydat")
pygame 2.6.1 (SDL 2.28.4, Python 3.11.12)
Hello from the pygame community. https://www.pygame.org/contribute.html

Contents

This returns a Python object that contains all of the trial conditions and results as attributes. These attributes can be listed using the Python vars function:

[3]:
for var in vars(psydata):
    print(var, end=" | ")
name | autoLog | trialList | nReps | trialWeights | nTotal | nRemaining | method | thisRepN | thisTrialN | thisN | thisIndex | thisTrial | finished | extraInfo | seed | data | sequenceIndices | originPath | origin | _exp |

Conditions

The trial conditions are in trialList, each element in this list is a dict of trial conditions that defines a trial:

[4]:
psydata.trialList
[4]:
[{'weight': 2,
  'condition_timeout': 0.0,
  'num_targets': 8,
  'target_order': 'clockwise',
  'target_indices': '0 1 2 3 4 5 6 7',
  'add_central_target': True,
  'hide_target_when_reached': True,
  'show_target_labels': False,
  'target_labels': '0 1 2 3 4 5 6 7',
  'fixed_target_intervals': False,
  'target_duration': 5.0,
  'central_target_duration': 5.0,
  'pre_target_delay': 0.0,
  'pre_central_target_delay': 0.0,
  'pre_first_target_extra_delay': 0.0,
  'target_distance': 0.4,
  'target_size': 0.04,
  'central_target_size': 0.02,
  'show_inactive_targets': True,
  'ignore_incorrect_targets': True,
  'play_sound': True,
  'use_joystick': False,
  'joystick_max_speed': 0.02,
  'show_cursor': True,
  'cursor_size': 0.02,
  'show_cursor_path': True,
  'automove_cursor_to_center': False,
  'freeze_cursor_between_targets': False,
  'cursor_rotation_degrees': 0.0,
  'post_trial_delay': 0.0,
  'post_trial_display_results': False,
  'post_block_delay': 0.0,
  'post_block_display_results': True,
  'show_delay_countdown': True,
  'enter_to_skip_delay': True},
 {'weight': 2,
  'condition_timeout': 0.0,
  'num_targets': 6,
  'target_order': 'random',
  'target_indices': '2 5 3 0 1 4',
  'add_central_target': True,
  'hide_target_when_reached': True,
  'show_target_labels': False,
  'target_labels': '0 1 2 3 4 5 6 7',
  'fixed_target_intervals': False,
  'target_duration': 5.0,
  'central_target_duration': 5.0,
  'pre_target_delay': 1.0,
  'pre_central_target_delay': 0.0,
  'pre_first_target_extra_delay': 0.0,
  'target_distance': 0.4,
  'target_size': 0.04,
  'central_target_size': 0.02,
  'show_inactive_targets': True,
  'ignore_incorrect_targets': True,
  'play_sound': True,
  'use_joystick': False,
  'joystick_max_speed': 0.02,
  'show_cursor': True,
  'cursor_size': 0.02,
  'show_cursor_path': True,
  'automove_cursor_to_center': True,
  'freeze_cursor_between_targets': True,
  'cursor_rotation_degrees': 0.0,
  'post_trial_delay': 0.0,
  'post_trial_display_results': False,
  'post_block_delay': 0.0,
  'post_block_display_results': True,
  'show_delay_countdown': True,
  'enter_to_skip_delay': True}]

This can be more easily viewed if converted to a pandas DataFrame:

[5]:
pd.DataFrame(psydata.trialList)
[5]:
weight condition_timeout num_targets target_order target_indices add_central_target hide_target_when_reached show_target_labels target_labels fixed_target_intervals ... show_cursor_path automove_cursor_to_center freeze_cursor_between_targets cursor_rotation_degrees post_trial_delay post_trial_display_results post_block_delay post_block_display_results show_delay_countdown enter_to_skip_delay
0 2 0.0 8 clockwise 0 1 2 3 4 5 6 7 True True False 0 1 2 3 4 5 6 7 False ... True False False 0.0 0.0 False 0.0 True True True
1 2 0.0 6 random 2 5 3 0 1 4 True True False 0 1 2 3 4 5 6 7 False ... True True True 0.0 0.0 False 0.0 True True True

2 rows × 35 columns

The weight of a trial is how many times it should be repeated. This information is also stored in the trialWeights list, so for example trialList[i] will be repeated trialWeights[i] times

[6]:
psydata.trialWeights
[6]:
[2, 2]

A block consists of doing a trial with each condition in trialList weight times, where weight can be a different number for each trial, so the total number of trials done in a block is then given by

\[nTrials = \sum_i^{nConditions} weight[i]\]

This block is then repeared nReps times:

[7]:
psydata.nReps
[7]:
1

The total number of trials nTotal is then given by

\[nTotal = nReps \times \left( \sum_i^{nConditions} weight[i] \right)\]
[8]:
psydata.nTotal
[8]:
4

The condition used for a given trial iTrial and repetition number iRep is given by sequenceIndices[iTrial][iRep], which gives the index of the conditions used in the trialList list:

[9]:
psydata.sequenceIndices
[9]:
array([[0],
       [0],
       [1],
       [1]])

The method specifies the order in which the trials were done:

  • sequential

    • the same order as trialList

  • random

    • order of trials shuffled within each block

  • fullRandom

    • order of trials fully shuffled

[10]:
psydata.method
[10]:
'sequential'

Results

The results of the trials are in data which contains a dict of numpy arrays of recorded data:

[11]:
for key, value in psydata.data.items():
    print(key, value.shape)
ran (4, 1)
order (4, 1)
target_indices (4, 1)
target_pos (4, 1)
to_target_timestamps (4, 1)
to_target_num_timestamps_before_visible (4, 1)
to_center_timestamps (4, 1)
to_center_num_timestamps_before_visible (4, 1)
to_target_mouse_positions (4, 1)
to_center_mouse_positions (4, 1)
to_target_success (4, 1)
to_center_success (4, 1)
[12]:
colors = ["blue", "green", "red", "cyan", "magenta", "yellow", "black", "orange"]
nTrials, nReps = psydata.sequenceIndices.shape


def get_axs_row():
    row = 0
    for condition in range(len(psydata.trialList)):
        if not psydata.trialList[condition]["automove_cursor_to_center"]:
            row += 2 * psydata.trialWeights[condition]
        else:
            row += psydata.trialWeights[condition]
    return row


def get_fig_height():
    add = 0
    for condition in range(len(psydata.trialList)):
        if not psydata.trialList[condition]["automove_cursor_to_center"]:
            add += psydata.trialWeights[condition]
    return 6 * (nTrials + add) * nReps


fig, axs = plt.subplots(get_axs_row(), nReps, figsize=(6, get_fig_height()))
axs = np.reshape(
    axs, (get_axs_row(), nReps)
)  # ensure axs is a 2d-array even if nTrials or nReps is 1
for trial in range(nTrials):
    for rep in range(nReps):
        loc = (trial, rep)
        condition = psydata.sequenceIndices[loc]
        target_radius = psydata.trialList[condition]["target_size"]
        central_target_radius = psydata.trialList[condition]["central_target_size"]
        ax = axs[loc]
        ax.set_title(f"Trial {trial}, Rep {rep} [Condition {condition}]")
        for positions, target_pos, color in zip(
            psydata.data["to_target_mouse_positions"][loc],
            psydata.data["target_pos"][loc],
            colors,
        ):
            ax.plot(positions[:, 0], positions[:, 1], color=color)
            ax.add_patch(
                plt.Circle(
                    target_pos,
                    target_radius,
                    edgecolor="none",
                    facecolor=color,
                    alpha=0.1,
                )
            )

        # if condition "automove_cursor_to_center" is deselected, plot the line to center, fill the enclosed area and output the area
        if not psydata.trialList[condition]["automove_cursor_to_center"]:
            loc_area = (trial + nTrials, rep)
            ax_area = axs[loc_area]
            ax_area.set_xbound(-0.5, 0.5)
            ax_area.set_ybound(-0.5, 0.5)
            ax_area.set_title(
                f"area plot for Trial {trial}, Rep {rep} [Condition {condition}]"
            )
            print("---------------------------------------------")
            print(f"area of Trial {trial}, Rep {rep} [Condition {condition}]")
            for (
                to_target_mouse_positions,
                to_center_mouse_positions,
                target_pos,
                color,
            ) in zip(
                psydata.data["to_target_mouse_positions"][loc],
                psydata.data["to_center_mouse_positions"][loc],
                psydata.data["target_pos"][loc],
                colors,
            ):
                ax.plot(
                    to_center_mouse_positions[:, 0],
                    to_center_mouse_positions[:, 1],
                    color=color,
                )
                coords = np.concatenate(
                    (to_target_mouse_positions, to_center_mouse_positions)
                )
                polygon = Polygon(coords)
                lr_coords = np.concatenate((coords[:], to_target_mouse_positions[0:1]))
                lr = LineString(lr_coords)
                validation = lr.is_valid
                multi_LineString = unary_union(lr)
                area = 0
                for pg in polygonize(multi_LineString):
                    area += pg.area
                    ax_area.plot(*pg.exterior.xy, color=color)
                    ax_area.fill(*pg.exterior.xy, facecolor=color)
                ax_area.add_patch(
                    plt.Circle(
                        target_pos,
                        target_radius,
                        edgecolor="none",
                        facecolor=color,
                        alpha=0.1,
                    )
                )
                print(f"{color}, area: {area}")

            ax.add_patch(
                plt.Circle(
                    [0, 0],
                    central_target_radius,
                    edgecolor="none",
                    facecolor="black",
                    alpha=0.1,
                )
            )

plt.show()
---------------------------------------------
area of Trial 0, Rep 0 [Condition 0]
blue, area: 0.02384012774348422
green, area: 0.016878322187928677
red, area: 0.01241100823045268
cyan, area: 0.015122099828252057
magenta, area: 0.01838616683813443
yellow, area: 0.026584183527663462
black, area: 0.01402879923061912
orange, area: 0.009765195843465515
---------------------------------------------
area of Trial 1, Rep 0 [Condition 0]
blue, area: 0.012710585484282402
green, area: 0.022543002158882722
red, area: 0.008019782096372857
cyan, area: 0.008811416635490702
magenta, area: 0.014736532418141348
yellow, area: 0.014034243031060156
black, area: 0.011178626543209877
orange, area: 0.004766468942901244
../_images/notebooks_raw_data_23_1.png

Each of these is a nTrials x nReps 2d array, where each element of this array contains the results from the corresponding trial for this variable (which might itself be a single value, e.g. target_pos, or an array of values, e.g. timestamps)

Which set of conditions was used is given by the sequenceIndices entry in the same location

Plot of results for each trial

For example, a scatter plot of the mouse positions for each trial, labelled by the condition, trial number and repetition number:

Plot of all trials combined for each condition

Here we instead make one plot for each set of conditions in trialList, and super-impose all of the corresponding results:

[13]:
colors = ["blue", "green", "red", "cyan", "magenta", "yellow", "black", "orange"]
nConditions = len(psydata.trialList)
nTrials, nReps = psydata.sequenceIndices.shape
fig, axs = plt.subplots(nConditions, 1, figsize=(6, 6 * nConditions))
axs = np.reshape(axs, (nConditions))  # ensure axs is a 1d-array
for trial in range(nTrials):
    for rep in range(nReps):
        loc = (trial, rep)
        condition = psydata.sequenceIndices[loc]
        target_radius = psydata.trialList[condition]["target_size"]
        ax = axs[condition]
        ax.set_title(f"Condition {condition}")
        for positions, target_pos, color in zip(
            psydata.data["to_target_mouse_positions"][loc],
            psydata.data["target_pos"][loc],
            colors,
        ):
            ax.plot(positions[:, 0], positions[:, 1], color=color)
            ax.add_patch(
                plt.Circle(
                    target_pos,
                    target_radius,
                    edgecolor="none",
                    facecolor=color,
                    alpha=0.1,
                )
            )
plt.show()
../_images/notebooks_raw_data_26_0.png

Plot of mouse movements vs time

A plot of x, y, and distance from target versus time for a single move to/from a target

[14]:
def dist(xys, xy0):
    return np.sqrt(np.mean(np.power(xys - xy0, 2), axis=1))


fig, axs = plt.subplots(1, 2, figsize=(16, 8))

trial = 0
rep = 0
i_target = 3
loc = (trial, rep)
condition = psydata.sequenceIndices[loc]
target_radius = psydata.trialList[condition]["target_size"]
central_target_radius = psydata.trialList[condition]["central_target_size"]
for dest, ax in zip(["target", "center"], axs):
    positions = psydata.data[f"to_{dest}_mouse_positions"][loc][i_target]
    target = psydata.data["target_pos"][loc][i_target] if dest == "target" else [0, 0]
    times = psydata.data[f"to_{dest}_timestamps"][loc][i_target]
    ax.set_title(f"Mouse movements to {dest}")
    ax.plot(times, positions[:, 0], label="x")
    ax.plot(times, positions[:, 1], label="y")
    ax.plot(times, dist(positions, target), label="distance from target")
    ax.legend()
plt.show()
../_images/notebooks_raw_data_28_0.png