Import und Aufbereitung von Psychopy-Daten

Batch-Import

Psychopy und Pavlovia schreiben Ihre Daten im long-Format raus, die dem kommenden Beispiel ähneln:

read_csv('data/1_Experiment_Vorüberlegungen_2022_Feb_14_1902.csv') %>% 
  head()
New names:
Rows: 47 Columns: 20
── Column specification
──────────────────────────────────────────────────────── Delimiter: "," chr
(9): Bild, weiter_Willkommen.keys, Ergebnisse, Sicherheit, key_resp.key... dbl
(10): Ergebnisse_Loop.thisRepN, Ergebnisse_Loop.thisTrialN, Ergebnisse_L... lgl
(1): ...20
ℹ Use `spec()` to retrieve the full column specification for this data. ℹ
Specify the column types or set `show_col_types = FALSE` to quiet this message.
• `` -> `...20`
# A tibble: 6 × 20
  Bild       Ergebnisse_Loop.this…¹ Ergebnisse_Loop.this…² Ergebnisse_Loop.thisN
  <chr>                       <dbl>                  <dbl>                 <dbl>
1 <NA>                           NA                     NA                    NA
2 Tabellen/…                      0                      0                     0
3 Tabellen/…                      0                      1                     1
4 Tabellen/…                      0                      2                     2
5 Tabellen/…                      0                      3                     3
6 Tabellen/…                      0                      4                     4
# ℹ abbreviated names: ¹​Ergebnisse_Loop.thisRepN, ²​Ergebnisse_Loop.thisTrialN
# ℹ 16 more variables: Ergebnisse_Loop.thisIndex <dbl>,
#   weiter_Willkommen.keys <chr>, weiter_Willkommen.rt <dbl>,
#   Entscheidung.response <dbl>, Ergebnisse <chr>,
#   Sicherheit_Entscheidung.response <dbl>, Sicherheit <chr>,
#   key_resp.keys <chr>, key_resp.rt <dbl>, participant <dbl>, session <chr>,
#   date <chr>, expName <chr>, psychopyVersion <chr>, frameRate <dbl>, …

Die Daten oben kommen aus einer FOV-Studie aus dem WS21 in der untersucht wurde, wie sicher sich Proband:innen bei der Entscheidung für einen t-Test oder einen anderen Test basierend auf Levene- und KS-Test-p-Werten sind und wie sehr sie sich in ihrer Entscheidung sicher sind..

Für jeden Probanden wird dabei eine Datei erstellt, so dass der data-folder gerne mal wie in Abbildung 7.1 aussehen kann.

Abb 7.1: Ansicht des Daten-Ordners

Um Auswertungen vorzubereiten müssen diese einzelnen Dateien erst mal zusammengefügt werden.

Dazu brauchen wir erst mal einen Vektor mit allen csv-files im Ordner:

 # bei mir liegt der data-Ordner im Verzeichnis über dem in dem sich das Skript befindet
list.files(path = 'data/',
           pattern = 'csv') %>% 
  str()
 chr [1:51] "1_Experiment_Vorüberlegungen_2022_Feb_14_1902.csv" ...

Da bei mir ein relativer Pfad nötig ist würden die files so nicht gefunden werden, deswegen müssen wir noch das full.names-Argument auf TRUE setzen.

list.files(path = 'data/',
           pattern = 'csv',
           full.names = T) %>% 
  str()
 chr [1:51] "data//1_Experiment_Vorüberlegungen_2022_Feb_14_1902.csv" ...

Die so erstellten Pfade können wir dann im batch öffnen und einlesen. Dazu kann der wrapper map_dfr um map und bind_rows aus dem purrr-Paket helfen:

all_vpn <- list.files(path = 'data/',
           pattern = 'csv',
           full.names = T) %>% 
  map_dfr(~read_csv(.))
Error in `dplyr::bind_rows()`:
! Can't combine `..26$Ergebnisse_Loop.thisRepN` <double> and `..27$Ergebnisse_Loop.thisRepN` <character>.

Der Fehler kommt daher, dass einzelne Dokumente zu kurz sind und die Spaltentypen deswegen nicht erkannt werden. Hier gibt es drei Möglichkeiten mit dem Problem umzugehen:

Bei Psychopy kann der Fehler oben nur auftreten wenn

  1. etwas am Skript geändert wurde wodurch ein Datentyp in einer Datei nicht mehr stimmt.
  2. ein:e Proband:in vor dem ersten Trial eines Blocks abgebrochen hat.

Im ersten Fall habt Ihr ganz andere Probleme und solltet im Detail darüber nachdenken wie ihr das löst - oder den unschönen Weg (“The ugly”) wählen.

Im zweiten Fall könnt Ihr alle zu kleinen files filtern (“The good”) oder manuell (“The bad”) die Spalten definieren.

files <- tibble(
  path = list.files(
    path = 'data/',
    pattern = 'csv',
    full.names = T
  ),
  size = file.size(path)
)

files %>% 
  head()
# A tibble: 6 × 2
  path                                                      size
  <chr>                                                    <dbl>
1 data//1_Experiment_Vorüberlegungen_2022_Feb_14_1902.csv   6597
2 data//10_Experiment_Vorüberlegungen_2022_Feb_14_1902.csv  6617
3 data//11_Experiment_Vorüberlegungen_2022_Feb_14_1902.csv  6609
4 data//12_Experiment_Vorüberlegungen_2022_Feb_14_1902.csv  6598
5 data//13_Experiment_Vorüberlegungen_2022_Feb_14_1902.csv  6599
6 data//14_Experiment_Vorüberlegungen_2022_Feb_14_1902.csv  6594
all_vpn <- files %>% 
  filter(size > mean(size)) %>% 
  pull(path) %>% 
  map_dfr(~read_csv(.))

Die zweite Möglichkeit ist es, in read_csv die erwarteten Spalten-Typen anzugeben.

In meinem Fall sieht das so aus:

all_vpn <- list.files(path = 'data/',
           pattern = 'csv',
           full.names = T) %>% 
  map_dfr(~read_csv(.,col_types = 'cnnnncnncnccnnccccnl'))

Der uneleganteste Weg ist es erstmal alle Spalten als character zu importieren und dann die nötigen Spalten umzuwandeln:

all_vpn <- list.files(path = 'data/',
           pattern = 'csv',
           full.names = T) %>% 
  map_dfr(~read_csv(.,col_types = cols(.default = 'c')))

all_vpn %>% 
  glimpse()
Rows: 20,629
Columns: 23
$ Bild                             <chr> NA, "Tabellen/45.png", "Tabellen/11.p…
$ Ergebnisse_Loop.thisRepN         <chr> NA, "0", "0", "0", "0", "0", "0", "0"…
$ Ergebnisse_Loop.thisTrialN       <chr> NA, "0", "1", "2", "3", "4", "5", "6"…
$ Ergebnisse_Loop.thisN            <chr> NA, "0", "1", "2", "3", "4", "5", "6"…
$ Ergebnisse_Loop.thisIndex        <chr> NA, "44", "10", "32", "40", "16", "27…
$ weiter_Willkommen.keys           <chr> "space", NA, NA, NA, NA, NA, NA, NA, …
$ weiter_Willkommen.rt             <chr> "5.486211061477661", NA, NA, NA, NA, …
$ Entscheidung.response            <chr> NA, "1", "1", "1", "1", "1", "1", "1"…
$ Ergebnisse                       <chr> NA, "anderer Test", "anderer Test", "…
$ Sicherheit_Entscheidung.response <chr> NA, "1", "3", "4", "1", "3", "3", "1"…
$ Sicherheit                       <chr> NA, "sehr sicher", "unsicher", "gar n…
$ key_resp.keys                    <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
$ key_resp.rt                      <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
$ participant                      <chr> "1", "1", "1", "1", "1", "1", "1", "1…
$ session                          <chr> "001", "001", "001", "001", "001", "0…
$ date                             <chr> "2022_Feb_14_1853", "2022_Feb_14_1853…
$ expName                          <chr> "Experiment_Vorüberlegungen", "Experi…
$ psychopyVersion                  <chr> "2021.2.3", "2021.2.3", "2021.2.3", "…
$ frameRate                        <chr> "59.783176973314745", "59.78317697331…
$ ...20                            <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
$ RT                               <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
$ Accuracy                         <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, N…
$ group                            <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, N…

Aus diesem Datensatz brauchen wählen wir dann die Spalten aus, die wir brauchen und wandeln die Reaktionszeiten um (und filtern leere Zeilen).

df <- all_vpn %>% 
  select(participant, Entscheidung.response, Ergebnisse, Sicherheit_Entscheidung.response, Sicherheit)
glimpse(df)
Rows: 20,629
Columns: 5
$ participant                      <chr> "1", "1", "1", "1", "1", "1", "1", "1…
$ Entscheidung.response            <chr> NA, "1", "1", "1", "1", "1", "1", "1"…
$ Ergebnisse                       <chr> NA, "anderer Test", "anderer Test", "…
$ Sicherheit_Entscheidung.response <chr> NA, "1", "3", "4", "1", "3", "3", "1"…
$ Sicherheit                       <chr> NA, "sehr sicher", "unsicher", "gar n…
df <- df %>% 
  mutate(across(matches('response'), ~as.numeric(.)),
         participant = as.numeric(participant))

df <- df %>% 
  filter(!is.na(Ergebnisse))

df %>% 
  glimpse()
Rows: 2,205
Columns: 5
$ participant                      <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
$ Entscheidung.response            <dbl> 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2…
$ Ergebnisse                       <chr> "anderer Test", "anderer Test", "ande…
$ Sicherheit_Entscheidung.response <dbl> 1, 3, 4, 1, 3, 3, 1, 3, 1, 2, 2, 3, 1…
$ Sicherheit                       <chr> "sehr sicher", "unsicher", "gar nicht…

Mit dem so umgewandelten Datensatz können wir dann wie gewohnt weiterarbeiten.

Zum Beispiel können wir uns die durchschnittliche Sicherheit pro Proband:in und Entscheidung ausgeben lassen:

response_summary <- df %>% 
  group_by(participant, Ergebnisse) %>% 
  summarise(Sicherheit = mean(Sicherheit_Entscheidung.response))
`summarise()` has grouped output by 'participant'. You can override using the
`.groups` argument.
response_summary %>% 
  head()
# A tibble: 6 × 3
# Groups:   participant [3]
  participant Ergebnisse   Sicherheit
        <dbl> <chr>             <dbl>
1           1 anderer Test       2.65
2           1 t-Test             2.57
3           2 anderer Test       2.33
4           2 t-Test             2.52
5           3 anderer Test       2.18
6           3 t-Test             2.54

Zusammenfügen

In den meisten Fällen werdet Ihr neben Psychopy-Daten noch wo anders Fragebogen dargeboten haben, die Ihr mit den Daten zusammenfügen müsst.

In unserem Beispiel existiert eine .xlsx-Datei, die die Limesurvey-Ergebnisse beinhaltet.

Um die zusammengefassten Ergebnisse an diese anzufügen müssen wir sie zuerst importieren:

limesurvey_results <- openxlsx::read.xlsx('data/LimeSurvey Daten.xlsx')

limesurvey_results %>% 
  glimpse()
Rows: 50
Columns: 4
$ Bitte.geben.Sie.Ihr.Geschlecht.an. <chr> "weiblich", "weiblich", "männlich",…
$ Bitte.geben.Sie.Ihr.Alter.an.      <dbl> 26, 18, 21, 21, 32, 32, 29, 29, 24,…
$ `Was.machen.Sie.beruflich?`        <chr> "Studium", "Studium", "Psychotherap…
$ `VP-Nr`                            <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, …

Die Zusammenfassungen sind noch im long-Format, können also nicht so einfach mit den LS-Daten im wide-Format zusammengeführt werden. Also erst einmal pivotieren:

response_summary <- response_summary %>% 
  pivot_wider(names_from = 'Ergebnisse',
              values_from = 'Sicherheit')

response_summary %>% 
  head()
# A tibble: 6 × 3
# Groups:   participant [6]
  participant `anderer Test` `t-Test`
        <dbl>          <dbl>    <dbl>
1           1           2.65     2.57
2           2           2.33     2.52
3           3           2.18     2.54
4           4           2.29     2.57
5           5           2.24     2.54
6           6           1.94     2.48

Die beiden Ergebnisse können wir jetzt kombinieren:

vp_level_data <- response_summary %>% 
  left_join(limesurvey_results,
            by = c('participant' = 'VP-Nr'))

vp_level_data %>% 
  glimpse()
Rows: 49
Columns: 6
Groups: participant [49]
$ participant                        <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, …
$ `anderer Test`                     <dbl> 2.647059, 2.333333, 2.176471, 2.294…
$ `t-Test`                           <dbl> 2.571429, 2.518519, 2.535714, 2.571…
$ Bitte.geben.Sie.Ihr.Geschlecht.an. <chr> "weiblich", "weiblich", "männlich",…
$ Bitte.geben.Sie.Ihr.Alter.an.      <dbl> 26, 18, 21, 21, 32, 32, 29, 29, 24,…
$ `Was.machen.Sie.beruflich?`        <chr> "Studium", "Studium", "Psychotherap…

Auf den Daten können wir dann arbeiten:

vp_level_data %>% 
  select(where(is.numeric)) %>% 
  cor()
                              participant anderer Test      t-Test
participant                    1.00000000  -0.29763927 -0.01798315
anderer Test                  -0.29763927   1.00000000 -0.35251805
t-Test                        -0.01798315  -0.35251805  1.00000000
Bitte.geben.Sie.Ihr.Alter.an. -0.09077279  -0.07051619  0.08474383
                              Bitte.geben.Sie.Ihr.Alter.an.
participant                                     -0.09077279
anderer Test                                    -0.07051619
t-Test                                           0.08474383
Bitte.geben.Sie.Ihr.Alter.an.                    1.00000000

Aufgabe

  1. Im Repo zu diesem Skript ist eine zip-Datei mit simulierten Daten eines fiktiven Reaktionszeit-Experiments zu finden. Lade die Zip herunter und entpacke sie.

  2. Importiere die csv-Dateien in einen Datensatz

  3. Berechne Accuracy, mittlere Reaktionszeit und Standardabweichung der Reaktionszeit pro Proband:in

  4. Füge die Ergebnisse mit der LimeSurvey-xlsx zusammen und erstelle eine Zusammenfassung der Reaktionszeiten und Accuracy pro angegebenem Geschlecht