1 libraries

library('tidyverse')

2 Vorbemerkungen

2.1 Zum Tutorial

2.1.1 Allgemeines

Dieses Tutorial dient der Einführung in die Datenexploration und Datenvisualisierung mit R. Wir demonstrieren grundlegende Verfahren zur Abfrage, Manipulation, Analyse und Visualisierung eines Datensatzes und demonstrieren deren Anwendung in deskriptiv-statistischen Fragestellungen.

Der Schwerpunkt des Tutorials liegt in der Anleitung zur Umsetzung der oben beschriebenen Verfahren, nicht auf den statistischen Konzepten, die diesen zugrundeliegen. Daher ist eine Vertrautheit mit deskriptiv-statistischen Grundbegriffen von Vorteil. Wir versuchen jedoch, ein Mindestmaß an Hintergrundinformationen zur Verfügung zu stellen. Einzelne im Tutorial genannte Begriffe sind zu diesem Zweck mit Links versehen, mit denen eine kurze Erklärung aufgerufen werden kann.

R bietet eine Vielzahl an Paketen für unterschiedlichste Datenanalysen. Aus diesem Grund kann dasselbe Ziel in R meist auf unterschiedlichen Wegen erreicht werden. Wir fokussieren uns hier größtenteils auf die Arbeit mit den sogenannten “Tidyverse-Paketen”, speziell die Pakete dplyr und ggplot2. Das liegt daran, dass diese Pakete eine transparente und ökonomische Arbeitsweise erlauben. Es sei jedoch darauf hingewiesen, dass in der Arbeit mit R immer Fälle auftreten können, in denen ein bestimmtes Paket nicht gut geeignet ist. Es lohnt sich daher auf jeden Fall, sich mit den Basisfunktionen von R vertraut zu machen und ggf. weitere Pakete zu recherchieren, die hier nicht besprochen werden.

Konkrete Verfahren und deren Erklärung erscheinen im Tutorial ab Kapitel 3 orange hinterlegt. Es wird immer zunächst der Code erklärt, anschließend wir der Code gezeigt. Abschließend folgt eine Darstellung des Outputs, den der jeweilige Code produziert.

2.1.2 zum Mitarbeiten

Das Tutorial kann ihnen als einfaches Nachschlagewerk dienen, Sie können aber auch, was wir empfehlen, die einzelnen Schritte aktiv in R nachvollziehen. Zudem stellen wir vereinzelt Übungsaufgaben, die Sie in R bearbeiten können. Für diese Form der Mitarbeit müssen Sie zunächst die folgenden Schritte durchführen:

  • Installieren sie die neueste Version von R für ihr Betriebssystem, zum Download gelangen Sie hier . Wählen Sie einen CRAN-Mirror unter “Germany” (sofern Sie sich in Deutschland aufhalten), welcher, ist nebensächlich. Wählen Sie dann ihr Betriebsystem und folgen Sie den weiteren Anweisungen.
  • Wir empfehlen außerdem die Arbeit mit R-Studio! Es handelt sich hierbei um eine Arbeitsumgebung, die die Arbeit mit R übersichtlicher und einfacher macht. Die Code-Beispiele aus diesem Tutorial entsprechen der vereinfachten Syntax aus der R-Studio-Umgebung. Zum Download gelangen Sie hier . Wählen Sie erneut den Dowload für ihr Betriebsystem.
  • Öffnen Sie nach der Installation beider Programme R-Studio.
  • Öfnnen Sie in R-Studio eine neue Datei des Typs R Script. Nachfolgend eine kurze Übersicht über die
    Benutzeroberfläche:

Links oben befindet sich das Eingabefenster. Hier wird das Skript geschrieben. Links unten befindet sich das Ausgabefenster. Hier wird das Skript ausgeführt und dessen Output angezeigt. Rechts oben befindet sich der Workspace. Hier werden aktuelle Objekte angezeigt. Hier können auch Daten aus externen Quellen importiert werden.

  • Geben Sie im Eingabefenster ihres Skripts den Befehl install.packages(“tidyverse”) ein und führen sie ihn aus, indem sie den Code markieren oder an dessen Ende klicken, und anschließend strg + Enter drücken. Alternativ können sie auch auf das “Run”-Symbol rechts oben in ihrem Eingabefenster klicken. Die Tidyverse-Pakete werden anschließend installiert.
  • Geben Sie nach Abschluss der Installation den Befehl library(tidyverse) ein und führen Sie ihn auf gleichem Weg aus. Hiermit können Sie die Tidyverse-Pakete, mit denen wir im Tutorial arbeiten, in ihrem Skript verwenden.
  • Dieses Tutorial arbeitet mit simulierten Beispieldaten. Um die ab Kapitel 3 gezeigten Schritte aktiv nachvollziehen zu können, müssen Sie diese Daten erstellen. Den Code zur Generierung der Daten finden sie am Ende des Dokuments. Kopieren Sie diesen Code, fügen Sie ihn in Ihr Skript ein und führen Sie ihn aus. In Ihrem Workspace sollten sich nun mehrere Objekte befinden. Relevant für das Tutorial ist aber nur der Dataframe sim_prn_res (was so viel bedeuten soll wie simulation of pronoun resolution).

2.2 zu R

Diese kurze Einführung ist für die Mitarbeit im Tutorial nicht zwingend notwendig, kann aber helfen, die grundlegende Arbeitsweise von R besser zu verstehen. Für eine umfassendere Einführung in die Funktionsweise von R sowie in die R-Basis-Syntax sei hier auf das Tutorial Statistik in R in Slow Motion verwiesen, das auch den Installationsprozess und die Orientierung in der Benutzeroberfläche ausführlicher beschreibt, als dieses Tutorial.

R ist eine Programmiersprache, die auf das Arbeiten mit Datenmengen ausgerichtet ist. Wie im Verlauf dieses Tutorials ersichtlich wird, liegt der große Vorteil in der Nutzung von R darin, dass wir mit relativ wenig Aufwand umfangreiche Veränderungen an unserem Datensatz vornehmen können. Wir können beispielsweise mit wenigen Zeilen Code neue Variablen definieren, eine Teilmenge unserer Daten zu einem separaten Datensatz machen, oder die gesamte Struktur unseres Datensatzes grundlegend ändern. Würden wir solche Arbeiten manuell durchführen, wäre dies mit erheblichem Zeitaufwand und größerer Fehleranfälligkeit verbunden. Außerdem wäre unser Vorgehen im Nachhinein schwer nachvollziehbar, sowohl für uns als auch für andere Personen. Die Arbeit mit R ist also ökonomischer, weniger Fehleranfällig und transparent, da unser Code genau dokumentiert, was wir mit unseren Daten machen.

Ein Großteil der Arbeit mit R erfolgt mit den folgenden Begriffen, die wir nachfolgend informell erläutern.

  • Operatoren
  • Objekte
  • Befehle und Argumente

2.2.1 Operatoren

Hiermit sind im weitesten Sinne alle Symbole gemeint, die zwei Begriffe in eine bestimmte Beziehung zueinander setzen. Diese Beziehung kann arithmetischer oder logischer Art sein.

Arithmetische Operatoren entsprechen in R größtenteils den bekannten Symbolen. Nachfolgend einige Beispiele:

#Addition#
10 + 5 
## [1] 15
#Subtraktion# 
10 - 5 
## [1] 5
#Multiplikation#
10 * 5 
## [1] 50
#Division# 
10 / 5 
## [1] 2
#Potenzierung#
10 ^ 5 
## [1] 1e+05

Logische Operatoren stellen Beziehungen her, die entweder wahr oder falsch sind. Eine Aussage mit logischem Operator erhält von R also entweder den Wert TRUE oder den Wert FALSE. Nachfolgend einige Beispiele, beachten Sie den jeweiligen Wahrheitswert, den R einer Aussage zuordnet.

#Äquivalenz#
10 == 10 
## [1] TRUE
#Ungleichheit# 
10 != 10
## [1] FALSE
#größer als#
10 > 5
## [1] TRUE
#kleiner als#
10 < 5
## [1] FALSE
#größer oder gleich# 
10 >= 5
## [1] TRUE
#kleiner oder gleich# 
10 <= 5
## [1] FALSE

In der Gruppe der logischen Operatoren seien hier außerdem das logisch und & sowie das logische oder | genannt.

Zwei weitere wichtige Operatoren, die nicht in die oben genannten Kategorien gehören, sind der sogenannte Pipe-Operator %>%, den wir später genauer kennenlernen werden, und der Zuweisungsoperator <- bzw. =. Dieser ist, wie wir gleich sehen werden, zentral für das definieren von Objekten.

2.2.2 Objekte

Objekte können wir als Informationsträger im weitesten Sinne verstehen. Es handelt sich um Einheiten, die über einen Namen identifizierbar und abrufbar sind und eine bestimmte Information enthalten. Objekte werden mithilfe des Zuweisungsoperators definiert. Hierfür wird links vom Operator ein Objektname genannt und rechts des Operators der Code, der den Informationsgehalt des Objekts bestimmt.

Objekte können sich hinsichtlich Inhalt, Größe und Struktur voneinander unterscheiden. Wir zeigen nachfolgend einige Beispiele.

So können wir theoretisch ein Objekt fünf definieren, dessen Informationsgehalt aus der Ziffer 5 besteht. Wir betrachten das Objekt mit dem Befehl show().

fünf <- 5 
show(fünf)
## [1] 5

Wir können auch einen numerischen Vektor c() erstellen, der eine von uns angegebene Menge an Ziffern enthält, und diesen als Objekt speichern. Wir nennen das Objekt Zahlen.

Zahlen <- c(1,2,3,4,5,6,7,8,9)
show(Zahlen)
## [1] 1 2 3 4 5 6 7 8 9

Dasselbe können wir mit Wörtern machen. Unsere Liste c() enthält dann eine von uns definierte Menge an Wörtern. Wir nennen das Objekt Wörter.

Wörter <- c("eins","zwei","drei","vier","fünf","sechs","sieben","acht","neun")
show(Wörter)
## [1] "eins"   "zwei"   "drei"   "vier"   "fünf"   "sechs"  "sieben" "acht"  
## [9] "neun"

Schließlich können wir auch Objekte zu einem größeren Objekt, einem Dataframe, kombinieren. Dataframes sind der wohl gängigste Objekttyp. Es handelt sich hierbei um Tabellen mit einer beliebigen Anzahl an Variablen (Spalten) und Beobachtungen (Zeilen). Wir erstellen aus unseren Objekten Zahlen und Wörter nachfolgend einen Dataframe, den wir Zahlen_Wörter nennen. Wir verwenden hierfür den Befehl data.frame().

Zahlen_Wörter <- data.frame(Zahlen,Wörter)
show(Zahlen_Wörter)
##   Zahlen Wörter
## 1      1   eins
## 2      2   zwei
## 3      3   drei
## 4      4   vier
## 5      5   fünf
## 6      6  sechs
## 7      7 sieben
## 8      8   acht
## 9      9   neun

Dataframes werden häufig nicht erst in R erstellt, sondern als .csv Dateien (Beispielsweise aus Excel oder google Sheets) importiert.

R kennt verschiedene Datentypen und wendet diese meist automatisch auf ein gegebenes Objekt an. Im Folgenden eine kurze Liste der gängigsten und wichtigsten Datentypen.

  • numeric (num) - numerische, kontinuierliche Daten mit folgenden Subtypen:

    • integer (int) - ganze Zahlen
    • double (dbl) - Dezimalzahlen
  • kategoriale Daten:

    • chraracter (chr) - Buchstabensequenzen
    • factor - kategoriale Variable (in Zahlen oder Buchstaben)
    • logical - TRUE oder FALSE

Wir können den Datentyp eines Objekts mit dem Befehl str() abfragen. Wenn wir den Befehl beispielsweise auf unseren Dataframe Zahlen_Wörter anwenden, erhalten wir den folgenden Output.

str(Zahlen_Wörter)
## 'data.frame':    9 obs. of  2 variables:
##  $ Zahlen: num  1 2 3 4 5 6 7 8 9
##  $ Wörter: chr  "eins" "zwei" "drei" "vier" ...

Wir sehen, dass die erste Variable Zahlen Daten des Typs numeric (num) enthält, und die zweite Variable Wörter Daten des Typs character (chr).

2.2.3 Befehle und Argumente

Befehle sind vordefinierte Funktionen, die wir abrufen und auf ein gegebenes Argument anwenden können. Wir haben solche Verfahren oben bereits mehrfach angewendet. Ein Befehl eröffnet immer eine Leerstelle (), in der das gewünschte Argument genannt wird. Im vorherigen Abschnitt haben wir zum Beispiel den Befehl data.frame() aufgerufen, und ihm als Argumente die Objekte Zahlen und Wörter genannt. Unseren Befehl können wir also paraphrasieren als “Erstelle einen Dataframe aus den Objekten Zahlen und Wörter”. Auch unsere ursprünglichen Objekte haben wir mit einem Befehl erstellt, nämlich c(), der seine Argumente zu einem Vektor (Zahlen) oder zu einer Liste (Buchstaben) zusammenfügt.

Viele Befehle in R haben vordefinierte Argumente, die eine spezifische, explizite Angabe benötigen, oder einen Default-Wert wählen, dessen Änderung ebenfalls einer expliziten Angabe bedarf. Wenn wir beispielsweise ein Diagramm mit dem Befehl ggplot() erstellen, müssen wir im Befehl aes() Angaben zu den vordefinierten Argumenten x und y machen, also z.B. die Variable l1 der X-Achse zuordnen (x = l1) und die Variable reaction_time der Y-Achse (y = reaction_time). Entsprechende Fälle sehen wir im Tutorial vermehrt ab Kapitel 6.

Abschließend sei noch erwähnt, dass die Befehle, die wir nutzen können, von den Paketen abhängen, die wir installiert und importiert haben. Die oben gezeigten Befehle gehören zu den Basisfunktionen von R und sind somit per Default abrufbar. Andere Befehle sind erst nutzbar, wenn die entsprechenden Pakete installiert und importiert sind. Aus diesem Grund ist für die Mitarbeit in diesem Tutorial auch die Installation des Packet-Bundles tidyverse wichtig, da die meisten der nachfolgend gezeigten Befehle sonst nicht verwendbar wären.


3 Exploration des Datensatzes: Abfragen

Achtung! Für die Mitarbeit ab dieser Stelle müssen sich die Beispieldaten, deren Code Sie am Ende des Dokuments finden, in ihrem Workspace befinden! Falls das also noch nicht der Fall ist, kopieren Sie den Code in ihr Skript und führen Sie ihn aus.

Für dieses Tutorial verwenden wir simulierte Daten. Diese Daten könnten aus einem Experiment zur Interpretation von Personalpronomen durch Lernerinnen des Deutschen stammen. Nehmen wir an, dass die Versuchspersonen in diesem hypothetischen Experiment eines von zwei Bildern auswählen sollten, um anzugeben, auf welchen von zwei möglichen Referenten sich ein Pronomen bezieht, das in einer zuvor gehörten Geschichte genannt wurde. Gemessen wurde die Zeit, die Versuchspersonen brauchten, um ein Bild auszuwählen.

Diese Aufgabe wurde unter verschiedenen Bedingungen erledigt, die sich dahingehend unterschieden, welche Information zur korrekten Auflösung des Pronomens benutzt werden konnten.

Außerdem wurde bei jeder Versuchsperson eine Sprachstandsmessung durchgeführt. Es liegen dementsprechend Daten vor, die jede Versuchsperson anhand mehrerer Merkmale beschreiben können und - was im Folgenden wichtig wird - es erlauben, die Versuchspersonen verschiedenen Gruppen zuzuordnen.

3.1 Globale Abfragen: Spalten und Zeilen

Unsere Daten sind im Dataframe sim_prn_res enthalten. Wir betrachten diesen Dataframe zunächst auf globaler Ebene.

  • Mit dem Befehl head() werden uns die Variablen unseres Dataframes angezeigt.
head(sim_prn_res)
##   subject       l1 age test_score item condition correct reaction_time
## 1       1 Englisch  25         84    1     Genus       1      649.9771
## 2       1 Englisch  25         84    2     Genus       0      610.0037
## 3       1 Englisch  25         84    3     Genus      NA            NA
## 4       1 Englisch  25         84    4     Genus       0      599.0006
## 5       1 Englisch  25         84    5     Genus       1      570.7394
## 6       1 Englisch  25         84    6     Genus       0      605.2639

Der Output dieses Befehls zeigt uns, dass unser Dataframe 8 Variablen enthält:

  • Die Kennung der Versuchsperson (= subject),
  • Die L1 der Versuchsperson (= l1 ),
  • Das Alter der Versuchsperson (= age),
  • Der Wert, den die Versuchsperson im unabhängigen Sprachstandsmessverfahren erreicht hat (= test_score),
  • Die Kennung des experimentellen Items, also der jeweils gehörten Geschichte und der jeweils präsentierten Bilder (= item),
  • Die experimentelle Bedingung, also die sprachliche Information, die zur Pronomenauflösung genutzt werden musste (= condition),
  • Die Antwort der Versuchsperson, also, ob das richtige Bild ausgewählt wurde (=1) oder nicht (=0) (= correct),
  • Die Reaktionszeit der Versuchsperson, also die Zeit, die zwischen Präsentation der Bilder und der Entscheidung der Versuchsperson vergangen ist (= reaction_time).

Uns interessiert nun, wie viele Beobachtungen unser Dataframe enthält.

  • Der Befehl nrow() gibt uns die Anzahl an Zeilen in unserem Dataframe aus.
nrow(sim_prn_res)
## [1] 900

Wir sehen also, dass unser Datensatz 900 Beobachtungen enthält.

3.2 Spezifische Abfragen: Experimentelle Bedingungen und L1

Im Folgenden wollen wir uns noch etwas genauer mit den einzelnen Variablen unseres Dataframes vertraut machen.

Für gezielte Abfragen zu einzelnen Variablen eines Dataframes gibt es in r verschiedene Möglichkeiten. Wir verwenden hier die Piping-Methode der tidyverse-Pakete, da wir diese im weiteren Verlauf immer wieder brauchen werden.

  • Indem wir das Pipe-Symbol %>% einem Befehl oder Objekt nachstellen, wird eine nachfolgende Operation immer auf den Output des vorherigen Befehls oder auf das vorherige Objekt angewendet. Wir verketten damit unseren Dataframe mit dem Befehl in der nächsten Zeile, schreiben also sim_prn_res %>% .
  • Der Befehl distinct() gibt alle Ausprägungen einer Variable aus. Damit können wir festellen, welche Bedingungen es in unserem Experiment gab, oder welche Erstsprachen die Versuchspersonen sprechen. Die Argumente des Befehls sind also jeweils die Variablen condition und l1.

sim_prn_res %>% 
  distinct(condition)
##        condition
## 1          Genus
## 2       Semantik
## 3 Genus+Semantik
sim_prn_res %>%  
  distinct(l1)
##                l1
## 1        Englisch
## 2  Niederl?ndisch
## 3        Russisch
## 4        Spanisch
## 5     Tschechisch
## 6     Italienisch
## 7        T?rkisch
## 8      Chinesisch
## 9       Kroatisch
## 10       Arabisch

Wir sehen, dass es drei experimentelle Bedingungen gab. Die Pronomenauflösung durch Genusinformation, durch semantische Information oder durch beide Informationen. Wir sehen auch die verschiedenen Erstsprachen der Versuchspersonen.

3.3 Befehlsverkettungen: Anzahl an Versuchspersonen und Items, Altersspanne

Nun ermitteln wir die Anzahl an Versuchspersonen und Items. Wir beginnen an dieser Stelle mit der Verkettung von Befehlen. Jeder Befehl steht unten in einer separaten Zeile.

  • Wir verketten zunächst wieder unseren Dataframe mit den nachfolgenden Zeilen.
  • Wir lassen uns mit distinct() die einzelnen Ausprägungen der Variable subject beziehungsweise item ausgeben und verketten den Output mit dem Befehl in der nächsten Zeile.
  • Wir verwenden nrow(), um uns die Anzahl an Versuchspersonen bzw. Items ausgeben zu lassen. Da wir den Befehl aus Zeile 2 mit diesem Befehl verkettet haben, wird der Output von distinct() als Input für nrow() erkannt. nrow() zählt jetzt also alle Zeilen des Outputs von distinct().

sim_prn_res %>%
  distinct(subject)%>%
  nrow()
## [1] 30
sim_prn_res %>%
  distinct(item)%>%
  nrow()
## [1] 30

Wir sehen, dass 30 Versuchspersonen je 30 Items bearbeitet haben. Da wir für dieses hypothetische Experiment ein within-subject-Design gewählt haben, hat jede VPn jede der drei experimentellen Bedingungen bearbeitet. Es wurden also jeweils 10 Items pro Bedingung durch jede Versuchsperson bearbeitet.

Uns interessiert abschließend die Altersspanne unserer Versuchspersonen. Diese ermitteln wir auf folgendem Weg.

  • Verkettung des Dataframes
  • Der Befehl select() extrahiert eine oder mehrere Variablen aus dem Dataframe. Wir extrahieren die Variable age. Achtung: Wenn wir in einer Befehlskette mit select() bestimmte Variablen extrahieren, können wir in nachfolgenden Befehlen derselben Kette nicht auf andere Variablen unseres Dataframes zugreifen!
  • Der Befehl range() gibt uns den untersten und den obersten Wert einer numerischen Variable aus. Wegen der Verkettung mit dem Befehl aus Zeile 2 wird der Befehl auf die Variable age angewendet.

sim_prn_res %>% 
  select(age) %>% 
  range()
## [1] 19 29

Wir sehen, dass Versuchspersonen in unserem Experiment zwischen 19 und 29 Jahren alt waren.

3.4 Komplexe Abfragen: Versuchspersonen pro L1

Nachdem wir uns einen Überblick über einzelne Variablen verschafft haben, wollen wir nun mehrere Variablen miteinander in Beziehung setzen. Wir wollen beispielsweise ermitteln, wie sich die Versuchspersonen auf die verschiedenen Erstsprachen verteilen. Wir müssen hierfür erneut die Anzahl an Versuchspersonen ermitteln (s.o.), allerdings müssen wir diesmal eine Gruppenvariable definieren, innerhalb welcher die Versuchspersonen gezählt werden. Wir gehen hierbei wieder zeilenweise vor. Wir verketten wieder sämtliche Befehle mit dem Pipe-Symbol %>%.

  • Verkettung des Dataframes.
  • Wir extrahieren mit select() die Variablen l1 und subject. Dieser Schritt ist für das weitere Verfahren nicht zwingend notwendig, kann aber helfen, innerhalb eines großen Datensatzes zu einem besseren Überblick zu gelangen.
  • Der Befehl group_by() definiert eine oder mehrere Gruppenvariablen. Das bewirkt, dass nachfolgende Befehle nicht einfach auf alle Daten eines Arguments angewendet werden, sondern separat für Gruppen, die sich aus den Ausprägungen der Gruppenvariable ergeben. Da uns die Anzahl an Versuchspersonen pro L1 interessiert, geben wir die Variable l1 als Argument an.
  • Wir lassen uns wieder mit distinct() die Ausprägungen der subject-Variable ausgeben. Da wir zuvor l1 als Gruppenvariable angegeben haben, erhalten wir neben den Ausprägungen von subject (1-30) zusätzlich eine Angabe zur L1.
  • Der Befehl count() generiert eine Variable n, die den Output von distinct()nach der Gruppenvariable l1 zusammenfasst: Es werden also alle Zeilen mit derselben Angabe zur L1 gezählt. Ohne unsere Gruppenvariable würde count() nur einen einzigen Wert ausgeben, nämlich die Gesamtzahl an Zeilen bzw. Versuchspersonen (= 30).

sim_prn_res %>%
  select(l1, subject)%>% 
  group_by(l1)%>%
  distinct(subject)%>%
  count()
## # A tibble: 10 × 2
## # Groups:   l1 [10]
##    l1                 n
##    <chr>          <int>
##  1 Arabisch           1
##  2 Chinesisch         2
##  3 Englisch           5
##  4 Italienisch        4
##  5 Kroatisch          3
##  6 Niederl?ndisch     6
##  7 Russisch           1
##  8 Spanisch           4
##  9 T?rkisch           2
## 10 Tschechisch        2

Der Output dieser Befehlskette zeigt uns zwei Variablen:

  • Die L1 der Versuchspersonen und
  • Die neu definierte Variable n, die die Anzahl an Versuchspersonen für jede L1 enthält.

Aufgabe1

Ermitteln Sie für jedes Alter, also jeden Wert der Variable age, die Anzahl an Versuchspersonen.

Lösung


4 Vorbereitung der Analyse: Komplexe Befehle

Nachdem wir uns vorangehend mit den grundlegenden Eigenschaften unseres Datensatzes beschäftigt haben, wollen wir nun unsere Datenanalyse vorbereiten. Unsere bisherigen Operationen enthielten immer einen Befehl pro Zeile, dessen Argumente jeweils eine oder mehrere Variablen waren. Wie gleich ersichtlich wird, müssen wir jedoch häufig komplexe Befehle schreiben, sodass das Argument eines Befehls ein weiterer Befehl ist. Wir versuchen nachfolgend, eine entsprechende Befehlshierarchie in unserer Erklärung durch eingerückte Stichpunkte darzustellen.

Wir verzichten in den nachfolgenden Erläuterungen darauf, auf die initiale Verkettung unseres Dataframes sim_prn_res %>% hinzuweisen .

4.1 Erfassen und Entfernen fehlender Werte

In einem ersten Schritt wollen wir die Zeilen aus unserem Datensatz entfernen, die fehlende Werte, sogenannte NAs, enthalten. Hiervon betroffen sind die Variablen correct und reaction_time. Wir führen zunächst eine Abfrage durch, die uns die Anzahl an NAs ausgibt. Wir können hierfür entweder die Variable correct oder reaction_time benutzen, da fehlende Werte bei beiden Variablen jeweils in derselben Zeile auftreten.

4.1.1 Erfassen von NAs

  • Wir verwenden count(), um die Anzahl an Zeilen mit fehlendem Wert zu ermitteln. Der Befehl benötigt dafür das folgende Argument:
    • Der Befehl is.na() gibt für jede Zeile einer Variable an, ob der Wert fehlt (= TRUE) oder vorhanden ist (= FALSE). Unsere Inputvariable ist hierbei entweder correct oder reaction_time. Wenn wir den Befehl als Argument von count() benutzen, erhalten wir also jeweils die Anzahl an Zeilen mit und ohne Wert.

sim_prn_res %>% 
  count(
    is.na(reaction_time))
##   is.na(reaction_time)   n
## 1                FALSE 842
## 2                 TRUE  58

Der Output zeigt uns, dass im gesamten Datensatz 58 Zeilen mit fehlendem Wert vorliegen.

Da wir die NAs nicht einmalig, sondern dauerhaft entfernen wollen, überschrieben wir unseren Dataframe mit dem entstehenden Code. Wir gehen wiefolgt vor:

4.1.2 Entfernen von NAs

  • Mit dem Zuordnungssymbol <- oder dem Gleichheitssymbol können wir den Output des nachfolgenden Befehls als Objekt speichern. Hierfür wird links vom Symbol ein Objektname definiert. Wir nutzen diese Funktion, um sim_prn_res zu überschreiben.
  • Der Befehl filter() extrahiert alle Zeilen aus unserem Datensatz, die den Angaben seines Arguments entsprechen.
    • Als Argument von filter() verwenden wir !is.na(). Das Ausrufezeichen fungiert in R als Negationsoperator. Wir extrahieren aus unserem Dataframe somit alle Zeilen, die keine NAs enthalten.

sim_prn_res <- 
  sim_prn_res %>%
  filter(
    !is.na(reaction_time))
#Anschließende Abfrage der Größe des Dataframes# 

nrow(sim_prn_res)
## [1] 842

Eine Abfrage zur Größe unseres Dataframes zeigt, dass dieser nur noch über 842 Zeilen (= 900 - 58) verfügt. Wir haben die fehlenden Werte also erfolgreich entfernt.

4.2 Änderung des Variablentyps

Objekte mit vielen verschiedenen numerischen Werten werden von R als kontinuierliche Variablen gelesen. Es wird also angenommen, dass die Werte auf einer numerischen Skala stehen (z.B. Alter: 25 Jahre < 29 Jahre). Da unsere Variablen subject und item numerische Werte enthalten, werden sie bislang ebenfalls als kontinuierliche Variablen gelesen, wie eine Abfrage des Datentyps mit dem Befehl str() zeigt.

sim_prn_res %>% 
  select(item, subject) %>% 
  str()
## 'data.frame':    842 obs. of  2 variables:
##  $ item   : int  1 2 4 5 6 7 8 9 11 12 ...
##  $ subject: int  1 1 1 1 1 1 1 1 1 1 ...

Die Abfrage gibt für beide Variablen den Datentyp integer (= int) aus, also eine numerische Variable ohne Nachkommastellen.

Die Benennung 1-30 ist aber willkürlich und dient lediglich der Identifikation einzelner Versuchspersonen. Wir wollen daher, dass beide Variablen fortan als Faktoren, also als kategoriale Variablen gelesen werden. Hierbei gehen wir wiefolgt vor:

  • Wir bezwecken erneut eine dauerhafte Änderung und überschreiben unseren Dataframe daher durch das Zuordnungssymbol <-
  • Mit dem Befehl mutate() können wir eine neue Variable definieren, die in ihrer Länge (Zeilen) mit bereits bestehenden Variablen übereinstimmt. Der nachfolgend erläuterte Code bildet das Argument des Befehls:
    • Wir überschreiben die bestehende Variablen subject und item, indem wir den Output des nachfolgenden Codes mit dem Zuordnungs- oder Gleichheitssymbol einem Objektnamen zuordnen (subject =, level =).
      • Wir verwenden den Befehl factor(), um eine kontinuierliche Variable zu einem Faktor umzuwandeln. Als Argumente geben wir jeweils die Variablen subject und item an.

sim_prn_res <- 
  sim_prn_res %>%
  mutate(subject = factor(subject),
         item = factor(item))
#Anschließende Abfrage des Datentyps#

sim_prn_res %>% 
  select(item, subject) %>% 
  str()                 
## 'data.frame':    842 obs. of  2 variables:
##  $ item   : Factor w/ 30 levels "1","2","3","4",..: 1 2 4 5 6 7 8 9 11 12 ...
##  $ subject: Factor w/ 30 levels "1","2","3","4",..: 1 1 1 1 1 1 1 1 1 1 ...

Eine erneute Abfrage des Datentyps zeigt, dass beide Variablen nun als Faktoren mit je 30 Ausprägungen gelesen werden.

Hinweis zur Handhabung von Variablentypen in R

Die Frage, welchen Datentyp wir einer gegebenen Variable in R zuweisen wollen, hängt nicht allein davon ab, um welche Art von Variable es sich handelt, sondern vor allem davon, was wir mit den Daten dieser Variable in R machen wollen! Für unsere Analyse ist es beispielsweise wichtig, dass R die Kodierung der Variablen subject und item als kategoriale Kodierung versteht. Manchmal liegt aber das Gegenteil vor, nämlich, dass eine eigentlich kategoriale Variable von R als numerische Variable verstanden werden soll.

Als Beispiel kann uns hier die Variable correct unseres Datensatzes dienen, die von R den Datentyp numeric erhält. Eigentlich handelt es sich auch hier um eine kategoriale Variable, da die numerische Kodierung 1 vs. 0 auf Kategorien verweist (korrekte Antwort vs. falsche Antwort). Wir behalten den Datentyp numeric hier aber bei, da wir später mit dieser Variable rechnen müssen. Um die Anzahl korrekt beantworteter Trials zu ermitteln, müssen wir alle Zeilen der Variable correct aufsummieren (siehe Kapitel 5). Die hieraus resultierende Summe ergibt sich logischerweise nur aus den Zeilen, die mit 1 kodiert wurden, also eine korrekte Antwort enthalten. Rechenoperationen sind mit Daten des Typs factor nicht möglich. Obwohl also eine kategoriale Variable vorliegt, müssen wir sie zum Zweck unserer Analyse wie eine numerische Variable behandeln.

4.3 Erstellen neuer Variablen

Unser Dataframe enthält Scores aus einem hypothetischen Sprachstandsmessverfahren, in dem zwischen 0 und 100 Punkten erreicht werden konnten. Wir wollen diesen Score später mit der Performanz in unserem Experiment in Beziehung setzen. Hierbei wollen wir den Sprachstand nicht nur als kontinuierliche, sondern auch als kategoriale Variable betrachten, die wir allerdings noch erstellen müssen. Wir lassen uns zunächst mit range() die Spanne der Werte ausgeben, die unsere Versuchspersonen erreicht haben.

sim_prn_res %>% 
  select(test_score) %>% 
  range()
## [1] 60 99

Wir nehmen an dieser Stelle an, dass die Skala eine verlässliche Inferenz auf das GER-Niveau erlaubt, sodass ein Score zwischen 60 und 73 Punkten dem Niveau B1 entspricht, ein Score zwischen 74 und 86 Punkten dem Niveau B2 und ein Score ab 87 Punkten dem Niveau C1.

Wir wollen nun eine Variable erstellen, die jede Versuchsperson auf Grundlage ihres Testscores einem Sprachniveau zuordnet. Wir erstellen also eine kategoriale Variable auf Grundlage einer kontinuierlichen Variable. Wir beabsichtigen wieder eine dauerhafte Änderung, überschreiben unseren Dataframe also wie in den vorherigen Beispielen.

  • Wir verwenden erneut mutate() und definieren darin eine neue Variable level (level =) mit dem nachfolgenden Code:
    • Der Befehl case_when() erlaubt es uns, verschiedene Ausprägungen für unsere neue Variable zu definieren:
      • Die Ausprägung B1 wird definiert als ein Wert kleiner oder gleich 73 der Varibale test_score.
      • Die Ausprägung B2 wird definiert als ein Wert größer 73 und kleiner oder gleich 86 der Variable test_score.
      • Die Ausprägung C1 wird definiert als ein Wert größer 86 der variable test_score.

sim_prn_res <-  
  sim_prn_res %>%
  mutate(level = case_when(test_score <= 73 ~ 'B1',
                           test_score > 73 & test_score <= 86 ~ 'B2',
                           test_score > 86 ~ 'C1'))
#Anschließende Abfrage der Variablen des Dataframes#

head(sim_prn_res)
##   subject       l1 age test_score item condition correct reaction_time level
## 1       1 Englisch  25         84    1     Genus       1      649.9771    B2
## 2       1 Englisch  25         84    2     Genus       0      610.0037    B2
## 3       1 Englisch  25         84    4     Genus       0      599.0006    B2
## 4       1 Englisch  25         84    5     Genus       1      570.7394    B2
## 5       1 Englisch  25         84    6     Genus       0      605.2639    B2
## 6       1 Englisch  25         84    7     Genus       1      588.0266    B2

Wenn wir nun die Variablen unseres Dataframes mit head() abrufen, sehen wir die neue Variable level.

Wir wollen abschließend sehen, wie sich unsere Versuchspersonen auf die verschiedenen Sprachniveaus verteilen. Wir führen hierfür eine gruppierte Zusammenfassung durch, wie wir sie oben bereits für die Anzahl an Versuchspersonen pro L1 durchgeführt haben (s. 3.4). Anstatt der Variable l1 gruppieren wir nun nach unserer neuen Variable level.

sim_prn_res %>% 
  select(subject,level) %>% 
  group_by(level) %>% 
  distinct(subject) %>% 
  count()
## # A tibble: 3 × 2
## # Groups:   level [3]
##   level     n
##   <chr> <int>
## 1 B1        8
## 2 B2       11
## 3 C1       11

Wir sehen, dass für das Niveau B1 acht VPn und für die Niveaus B2 und C1 jeweils 11 VPn vorliegen.


Aufgabe 2

Ermitteln Sie die Spannweite (range) der Variable test_score für alle Beobachtungen, in denen die Reaktionszeit kürzer ist als 500ms. Führen Sie dieselbe Abfrage für alle Beobachtungen durch, in denen die Reaktionszeit länger ist als 500ms.

Lösung

Aufgabe 3

Fügen Sie sim_prn_res eine neue Variable category mit den zwei Ausprägungen go und no_go hinzu. Geben Sie für diese Ausprägungen jeweils die folgenden Bedingungen an (die Variable ergibt übrigens keinerlei Sinn und ist nur zur Übung gedacht):

Stufe 1:

  • Alle Beobachtungen mit der L1 “Englisch” = go
  • Alle anderen Beobachtungen = no_go

Stufe 2:

  • Alle Beobachtungen mit der L1 “Englisch” und Reaktionszeit über 600ms = go
  • Alle anderen Beobachtungen = no_go

Tipp: Für Stufe 2 benötigen Sie die Operatoren logisches und & und logisches oder |.

Lösung


5 Deskriptive Analyse

Wir beginnen nun mit einer Analyse der Variable correct, die für jedes Trial angibt, ob eine Versuchsperson das Pronomen korrekt aufgelöst hat (1 vs. 0). Uns interessiert hierbei, in welcher Beziehung das Antwortverhalten von Versuchspersonen zu ihrem Sprachstand und zu der jeweiligen experimentellen Bedingung steht. Wir verzichten nachfolgend darauf, auf die optionale Selektion der Variablen von Interesse durch select() hinzuweisen, behalten das Vorgehen im Code jedoch bei.

5.1 Anteil korrekter Trials pro Versuchsperson und Testscore

Wir wollen zunächst für jede Versuchsperson den Anteil korrekter Antworten ermitteln und diesen mit dem Testscore der Versuchspersonen in Beziehung setzen. Uns interessiert also zunächst, ob eine gerichtete Beziehung zwischen den beiden Werten vorliegt. Wir gehen hierbei wiefolgt vor:

  • Wir verwenden group_by(), um subject und test_score als Gruppenvariablen anzugeben.
  • Der Befehl reframe() erlaubt uns das Erstellen neuer Variablen, die in ihrer Länge nicht mit den bisherigen Variablen übereinstimmen. Wir definieren innerhalb von reframe() die folgenden Variablen:
    • s ist die absolute Zahl korrekter Antworten. Wir ermitteln diese mit dem Befehl sum() und dem Argument (correct == 1). Hierdurch werden die Werte aller Zeilen aus correct addiert, sofern sie “1” entsprechen. Wir erhalten somit effektiv die Anzahl korrekter Trials.
    • N ist die absolute Zahl an Trials, die eine Versuchsperson bearbeitet hat. Wir ermitteln sie mit dem Befehl length(), der die Anzahl an Werten innerhalb eines Objekts ausgibt. Als Argument geben wir subject an, sodass jeweils die Anzahl an Zeilen gezählt wird, die derselben Versuchsperson entsprechen.
    • p ist der Anteil korrekter Antworten an allen Trials einer Versuchsperson. Die Variable ist also der Quotient aus s und N und wird hier durch round(…,2) auf die zweite Nachkommastelle gerundet.
  • Mit dem Befehl arrange() ordnen wir den Output nach der Variable test_score in aufsteigender Reihenfolge.

sim_prn_res %>% 
  select(subject, test_score, correct) %>% 
  group_by(test_score, subject) %>% 
  reframe(s = sum(correct==1), 
          N = length(subject), 
          p = round(s/N, 2)) %>% 
  arrange(test_score)
## # A tibble: 30 × 5
##    test_score subject     s     N     p
##         <int> <fct>   <int> <int> <dbl>
##  1         60 26         26    30  0.87
##  2         61 24         20    27  0.74
##  3         65 12         18    29  0.62
##  4         66 15         19    28  0.68
##  5         68 14         14    27  0.52
##  6         69 22         18    26  0.69
##  7         70 21         24    28  0.86
##  8         72 5          23    28  0.82
##  9         74 28         22    28  0.79
## 10         75 20         25    26  0.96
## # ℹ 20 more rows

Im Output unserer Befehlskette liegen nur noch diejenigen Variablen vor, die wir unter group_by() spezifiziert oder unter reframe() definiert haben. Der Output enthält zudem nur noch 30 Zeilen, die jeweils einer Versuchsperson entsprechen.

Wir erstellen mit reframe() also nicht einfach eine neue Variable innerhalb einer bestehenden Datenstruktur, wie vorangehend mit mutate() (s. 4.2. & 4.3.), sondern ändern die grundlegende Datenstruktur selbst. Wir müssen uns daher zuvor überlegen, welche unserer ursprünglichen Variablen, in unserem Fall z.B. test_score, im Output erhalten bleiben sollen.

Der Vorliegende Output kann nun dahingehend untersucht werden, ob der Anteil korrekter Trials bei steigendem Testscore systematisch zunimmt. Wir greifen die Frage im nächsten Kapitel erneut auf.

5.2 Gemittelter Anteil korrekter Trials pro Bedingung nach Sprachniveau

Wir wollen das Antwortverhalten unserer Versuchspersonen nun mit den verschiedenen experimentellen Bedingungen in Beziehung setzen. Wir wollen hierbei erneut die Rolle des Sprachstands für die Performanz berücksichtigen. Uns interessiert also, ob und inwieweit sich die Performanzen in den verschiedenen Bedingungen in Abhängigkeit des Sprachstands voneinander unterscheiden. Um eine überschaubare Menge an Vergleichsmomenten zu gewährleisten, greifen wir auf die vorangehend erstellte, kategoriale Variable level zurück. Wir bestimmen zunächst für jede Versuchsperson den Anteil korrekter Antworten pro Bedingung (Schritt 1) und bilden auf dieser Grundlage Mittelwerte für die drei Sprachniveaus (Schritt 2).

5.2.1 Schritt 1

  • Wir geben unter group_by() die Variablen subject, level und condition an. Die nachfolgend definierten Werte werden also pro Versuchsperson und pro Bedingung ermittelt. Da eine Versuchsperson immer nur einem Sprachniveau zugeordnet wird, hat die Inklusion der Variable level zunächst keinen Einfluss auf die ermittelten Werte. Wir müssen die Variable aber für Schritt 2 beibehalten und geben sie daher ebenfalls unter group_by() an.
  • Wir definieren unter reframe() erneut die Variablen s, N und p. Da wir condition als zusätzliche Gruppenvariable eingeführt haben, ergeben sich jedoch die folgenden Änderungen:
    • s ergibt sich nicht mehr aus der Summe aller korrekter Trials derselben Versuchsperson, sondern aus der Summe aller korrekten Trials derselben Versuchsperson in derselben experimentellen Bedingung.
    • N ergibt sich nicht mehr aus der Anzahl an Trials derselben Versuchsperson, sondern aus der Anzahl an Trials derselben Versuchsperson in derselben Bedingung.
    • p gibt folglich den Anteil korrekter Trials derselben Versuchsperson innerhalb derselben Bedingung an.

#Schritt 1#
sim_prn_res %>%
  select(subject, level, condition, correct)%>%
  group_by(subject, level, condition)%>%
  reframe(s = sum(correct=='1'), 
          N = length(subject), 
          p = round(s/N,2))
## # A tibble: 90 × 6
##    subject level condition          s     N     p
##    <fct>   <chr> <chr>          <int> <int> <dbl>
##  1 1       B2    Genus              5     8  0.62
##  2 1       B2    Genus+Semantik     9     9  1   
##  3 1       B2    Semantik           9    10  0.9 
##  4 2       B2    Genus              6     9  0.67
##  5 2       B2    Genus+Semantik     7     9  0.78
##  6 2       B2    Semantik           9    10  0.9 
##  7 3       B2    Genus              8     9  0.89
##  8 3       B2    Genus+Semantik     8    10  0.8 
##  9 3       B2    Semantik           9    10  0.9 
## 10 4       C1    Genus              9     9  1   
## # ℹ 80 more rows

Der Output dieses Arbeitsschrittes enthält also nicht 30, sondern 90 Zeilen, da für jede Versuchsperson Werte aller drei Bedingungen vorliegen.

Der nachfolgend beschriebene Code ist unten ab #Schritt 2# zu sehen.

5.2.2 Schritt 2

  • Wir verwenden erneut group_by() und geben diesmal level und condition als Gruppenvariablen an. Die nachfolgend definierten Kennwerte werden also für jede Kombination aus Sprachniveau und Bedingung ermittelt.
  • Wir verwenden reframe() und bestimmen folgenden Kennwerte:
    • n ist die Anzahl an Versuchspersonen pro Bedingung und Sprachniveau und wird erneut mit length() ermittelt.
    • m ist das arithmetische Mittel von p, also dem Anteil korrekter Antworten pro Bedingung und Sprachniveau, und wird mit dem Befehl m() berechnet.
    • sd ist die Standardabweichung von p und wird hier mit dem Befehl sd() berechnet.
    • se ist der Standardfehler von m und wird hier manuell berechnet als der Quotient aus sd und n.
    • t ist der Wert für das 97,5%-Quantil der jeweils passenden t-Verteilung. Wir berechnen den Wert mit dem Befehl qt() und spezifizieren eine Wahrscheinlichkeit von 0.975 als Quantilwert sowie die Formel n-1 für die Bestimmung der Freiheitsgrade.
    • ci ist der Wert, mit dem ein 95%-Konfidenzintervall um m gelegt wird, und wird hier manuell berechnet als das Produkt von t und se.

#Schritt 1# 
sim_prn_res %>%
  select(subject, level, condition, correct)%>%
  group_by(subject, level, condition)%>%
  reframe(s = sum(correct=='1'), 
          N = length(condition), 
          p = round(s/N,2))%>% 
#Schritt 2#
  group_by(level, condition)%>% 
  reframe(n=length(subject), 
          m = round(mean(p),2), 
          sd = round(sd(p),2), 
          se = round(sd/sqrt(n),2), 
          t = round(qt(0.975, df = n-1),2),
          ci = round(t*se,2))
## # A tibble: 9 × 8
##   level condition          n     m    sd    se     t    ci
##   <chr> <chr>          <int> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 B1    Genus              8  0.62  0.15  0.05  2.36  0.12
## 2 B1    Genus+Semantik     8  0.78  0.25  0.09  2.36  0.21
## 3 B1    Semantik           8  0.8   0.12  0.04  2.36  0.09
## 4 B2    Genus             11  0.75  0.16  0.05  2.23  0.11
## 5 B2    Genus+Semantik    11  0.85  0.12  0.04  2.23  0.09
## 6 B2    Semantik          11  0.89  0.09  0.03  2.23  0.07
## 7 C1    Genus             11  0.96  0.07  0.02  2.23  0.04
## 8 C1    Genus+Semantik    11  0.93  0.05  0.02  2.23  0.04
## 9 C1    Semantik          11  0.95  0.07  0.02  2.23  0.04

Als Output erhalten wir eine Tabelle mit neun Zeilen und sieben Spalten. Für jede Kombination von Sprachniveau und experimenteller Bedingung liegt eine Zeile vor, die jeweils die Stichprobengröße, den Mittelwert des Anteils korrekter Antworten, Standardabweichung, Standardfehler und den Wert für das Konfidenzintervall enthält. Wir können auf dieser Grundlage also die zentrale Tendenz im Antwortverhalten der Versuchspersonen nach Sprachniveau und Bedingung miteinander vergeichen. Die Streuungsmaße helfen uns dabei, die Vergleichbarkeit der Mittelwerte der verschiedenen Gruppen einzuschätzen.

Zu beachten ist, dass unsere Kennwerte über den Anteil korrekter Antworten pro Versuchsperson gebildet wurden. Unsere Streuungsmaße geben daher nur Aufschluss über die Homo- bzw. Heterogenität der Performanz unserer Versuchspersonen, aber nicht darüber, wie homo- bzw. heterogen die verschiedenen Items bearbeitet wurden. Hierfür müssten wir unsere Kennwerte auf Grundlage des Anteils korrekter Antworten pro Item berechnen.


Aufgabe 4

Führen Sie eine entsprechende by-Item Analyse durch.

Schritt 1:

  • Ermitteln Sie zuerst für jedes Item pro Bedingung und Sprachniveau den Anteil korrekter Antworten.

Schritt 2:

Ermitteln Sie anschließend pro Bedingung und Sprachniveau die folgenden Kennwerte:

  • Den Mittelwert des Anteils korrekter Antworten
  • Die Varianz des Anteils korrekter Antworten
  • Die Standardabweichung des Anteils korrekter Antworten
  • Den Wert für das 95%-Konfidenzintervall (denken Sie an die hierfür nötigen Koeffizienten)

Vergleichen Sie den Output der Schritte 1 und 2 mit dem Output der by-Subject Analyse. Welche Gemeinsamkeiten und welche Unterschiede liegen vor? Welche Kennwerte sind gleich, welche sind verschieden?

Lösung

  • by-Subject: 90 Zeilen, 3 Zeilen pro Versuchsperson, eine Zeile pro Bedingung
  • by-Item: 90 Zeilen, 3 Zeilen pro Item, eine Zeile pro Sprachniveau

  • Gleich: Mittelwert (bis auf zweite Nachkommastelle)
  • Unterschiedlich: Stichprobengröße (n), Varianz, Standardabweichung, Standardfehler, Konfidenzintervall

Gleiche zentrale Tendenz, unterschiedliche Streuung


6 Visualisierung der Analyse: ggplot2

Die Ergebnisse unserer vorangehenden Analysen können leichter Interpretiert werden, wenn wir diese in ein visuelles Format überführen. Grafiken können in R sowohl mit der Basis-Syntax als auch mit dem Paket ggplot2 erstellt werden. Wir verwenden nachfolgend ggplot2, da das Paket umfangreichere Gestaltungsmöglichkeiten bietet und zudem mit der Piping-Methode in eine Befehlskette integriert werden kann.

Mit dem Befehl ggplot() wird eine ggplot-Umgebung aufgerufen. Dieser initiale Befehl kann mit dem Piping-Symbol %>% an eine bestehende Befehlskette angehängt werden. Der ggplot-Umgebung können nun diverse graphische Eigenschaften durch einzelne Befehle zugeordnet werden. Diese Zuordnung erfolgt durch das Nachstellen des Pluszeichens an den vorangehenden Befehl (s.u.). Für das Erstellen von Grafiken benötigen ggplot-Objekte Inputdaten. Diese werden in einer Verkettung direkt dem Output der vorangehenden Befehlskette entnommen. Wenn eine ggplot-Umgebung außerhalb einer Befehlskette aufgerufen wird, muss der Input-Dataframe in ggplot() explizit gemacht werden.

6.1 Anteil korrekter Antworten und Testscore: Punktdiagramm + Regressionslinie

Wir beginnen mit der Analyse aus 5.1, in der wir für jede Versuchsperson den Anteil korrekter Antworten bestimmt haben. Wir erstellen nun ein einfaches Punktdiagramm, um die Beziehung zwischen dem Antwortverhalten und dem Testscore visuell nachvollziehen zu können.

Wir führen zunächst die Schritte aus 5.1 aus und stellen dieser Befehlskette das Piping-Symbol nach, um eine ggplot-Umgebung in die Befehlskette zu integrieren.

Der nachfolgend beschriebene Code ist unten ab #Visualisierung - Parameter# zu sehen.

  • Wir verwenden ggplot(), um eine ggplot-Umgebung aufzurufen.
    • Innerhalb von ggplot() bestimmen wir mit dem Befehl aes() (= aesthetics) die grundlegenden Parameter unserer Grafik: Wir ordnen mit dem Gleichheitszeichen die Variable test_score der X-Achse, und die Variable p der Y-Achse zu.
  • Mit dem Befehl geom_point() rufen wir ein Punktdiagramm auf. Das Diagramm übernimmt die oben definierten aes-Parameter und bildet somit für jeden Wert des Testscores (X-Achse) den entsprechenden Wert von p (Y-Achse) ab.
  • Mit dem Befehl geom_smooth wird eine Regressionslinie in die Grafik integriert, die uns hilft, den allgemeinen Trend im Antwortverhalten in Abhängigkeit des Testscores zu identifizieren.
    • Wir nehme einen linearen Zusammenhang zwischen beiden Variablen an, daher spezifizieren wir innerhalb von geom_smooth() die Methode lm (= linear model).
    • geom_smooth() legt standardmäßig ein Konfidenzintervall um die gezeigte
      Regressionslinie. Wir verzichten hierauf mit der Angabe se = FALSE.
  • theme_bw() ist eine von mehreren Darstellungmöglichkeiten unseres Koordinatensystems.

#Deskriptive Analyse#
sim_prn_res %>% 
  select(subject, test_score, correct)%>% 
  group_by(test_score, subject)%>% 
  reframe(s = sum(correct==1), 
          N = length(subject), 
          p = round(s/N, 2))%>% 
#Visualisierung#
ggplot(
  aes(x = test_score, 
      y = p))+
geom_point()+
geom_smooth(method = 'lm',
            se = FALSE)+
theme_bw() 

Das Diagramm liefert uns zwei wichtige Erkenntnisse: Einerseits steigt die Wahrhscheinlichkeit, korrekt zu antworten, mit dem Testscore der Versuchsperson. Im oberen Bereich der X-Achse liegen mehr Datenpunkte mit hohem Anteil korrekter Antworten vor, als in der niedrigeren Bereichen. Dieser Zusammenhang wird auch durch die positive Steigung der Regressionsgerade gezeigt. Allerdings wird die Performanz mit steigendem Tetsscore nicht nur besser, sondern auch homogener! Je weiter wir uns auf der X-Achse nach links bewegen, desto zerstreuter sind die Datenpunkte auf der Y-Achse. Wir müssen daher annehmen, dass neben dem Sprachstand noch andere Variablen vorliegen, die die Performanz der Versuchspersonen beeinflussen.

6.2 Gemittelter Anteil korrekter Antworten nach Sprachniveau und Bedingung: Balkendiagramm mit Fehlerbalken

Die vorangehende Analyse gibt uns Anlass zur Annahme, dass das Antwortverhalten einer Versuchsperson in Zusammenhang mit ihrem Sprachstand steht. Gleichzeitig konnten wir eine größere Streuung in den unteren Wertebereichen fesstellen, sodass wir annehmen können, dass der Testscore nicht die einzige Variable ist, die mit dem Antwortverhalten im Zusammenhang steht. Wir wollen daher nun die Rolle der experimentellen Bedingung in unsere Analyse mit einbeziehen. Hierfür können wir auf unsere Analyse aus 5.2 zurückgreifen.

Unser Ziel ist, ein Balkendiagramm zu erstellen, das für jedes Sprachniveau den gemittelten Anteil korrekter Antworten pro Bedingung zeigt. Wir wollen außerdem die relative Homo- bzw. Heterogenität der Performanz in den verschiedenen Bedingungen abbilden, um bessere Rückschlüsse über die Aussagekraft der beobachteten Unterschiede treffen zu können. Aus diesem Grund wollen wir Fehlerbalken in unser Diagramm integrieren, die für jeden Mittelwert das entsprechende Konfidenzintervall abbilden. Wir schreiben zunächst den Code aus 5.2 und stellen diesem das Piping-Symbol nach, um eine ggplot-Umgebung anzuhängen.

Der nachfolgend beschriebene Code ist unten ab #Visualisierung - Parameter# zu sehen.

  • Wir rufen mit ggplot() eine ggplot-Umgebung auf.
    • Wir bestimmen erneut mit aes() die Parameter unseres Diagramms: die Variable level (Sprachniveau) wird der X-Achse, die Variable m (Mittelwert) der Y-Achse zugeordnet. Wir spezifizieren zudem einen neuen Parameter fill, der uns ermöglicht, die Variable der X-Achse weiter zu differenzieren. Mit fill = condition können wir jedes Sprachniveau in die drei experimentellen Bedingungen unterteilen. Mit fill wird zudem automatisch eine farblich differenzierte Legende der angegebenen Variable erstellt.
  • Mit geom_bar() rufen wir ein Balkendiagramm auf. Das Diagramm übernimmt die aes-Parameter aus ggplot().
    • Mit stat = ‘identity’ geben wir an, dass die Werte aus m unverändert im Diagramm übernommen werden sollen.
    • Die Angabe position = ‘dodge2’ sorgt für einen Mindestabstand zwischen den Balken.
  • Mit geom_errorbar() integrieren wir Fehlerbalken in unser Diagramm. Der Befehl entnimmt den aes-Parametern aus ggplot(), dass für jeden X-Wert des Diagramms ein Fehlerbalken generiert wird. Es bedarf allerdings einer zusätzlichen Angabe über den Wertebereich der Fehlerbalken:
    • Wir rufen aes() auf und spezifizieren jeweils das untere (ymin) und das obere Ende (ymax) der Fehlerbalken. Da die Fehlerbalken das Konfidenzintervall abbilden sollen, definieren wir ymin als m - ci und ymax als m + ci.
    • Mit width = 0.3 passen wir die Breite der Fehlerbalken an.
    • Mit position = position_dodge(0.9) zentrieren wir die Fehlerbalken in Bezug auf die Balken unseres Diagramms.

#Deskriptive Analyse#
sim_prn_res %>%
  select(subject, level, condition, correct)%>%
  group_by(subject, level, condition)%>%
  reframe(s = sum(correct=='1'), N = length(subject), p = round(s/N,2))%>%
  group_by(level, condition)%>%
  reframe(n=length(subject), m = round(mean(p),2), sd = round(sd(p),2), se = round(sd/sqrt(n),2), 
          t = round(qt(0.975, df = n-1),2),ci = round(t*se,2))%>% 
#Visualisierung#  
ggplot(
  aes(x = level, 
      y = m, 
      fill = condition))+
geom_bar(stat = 'identity', 
         position = 'dodge2')+
geom_errorbar(
        aes(ymin = m-ci, 
            ymax = m+ci), 
        width = 0.3, 
        position = position_dodge(0.9))+
theme_bw()

Das Balkendiagramm zeigt uns einen grundsätzlichen Anstieg korrekter Antworten über die drei Sprachniveaus hinweg: C1-Versuchspersonen performen in allen drei Bedingungen am besten. Zudem ist ihre Performanz am homogensten, sowohl zwischen den einzelnen Bedingungen, wie die geringen Abstände zwischen den Balken zeigen, als auch innerhalb einer Bedingung, wie die relativ kleinen Konfidenzintervalle (Fehlerbalken) zeigen. Verglichen hiermit wir die Performanz mit absteigendem Sprachniveau nicht nur fehlerhafter sondern auch heterogener. Die Differenzen zwischen verschiedenen Bedingungen werden größer, außerdem werden die Konfidenzintervalle der Mittelwerte für einzelne Bedingungen größer, was auf sehr heterogene Performanz innerhalb einer Bedingung hinweist.

Im vorangehend erstellten Punktdiagramm (6.1) haben wir gesehen, dass die Performanz mit absteigendem Testscore immer weiter streut. Im Balkendiagramm können wir dasselbe beobachten, allerdings etwas detaillierter: Die größere Streuung wird teilweise durch die stärkeren Performanzunterschiede zwischen verschiedenen Bedingungen erklärt (vgl. Genus vs. Rest in B1), andererseits aber auch durch stärkere Streuung innerhalb einer Bedingung (vgl. das Konfidenzintervalll für Genus+Semantik in B1).

Das oben gezeigte Balkendiagramm vergleicht innerhalb jedes Sprachniveaus die gemittelte Performanz jeder experimentellen Bedingung. Wir können die Daten aber auch anders aufbereiten, sodass innerhalb derselben Bedingung die Performanz nach Sprachniveau verglichen wird. Hierfür müssen wir unseren Code lediglich in Bezug auf die zwei aes-Parameter x und fill in ggplot() ändern:

  • x = condition
  • fill = level

Diese Darstellung macht deutlich, dass die Performanz in der Genus-Bedingung am stärksten vom Sprachniveau einer Versuchsperson beeinflusst ist. Zum einen ist in dieser Bedingung der stärkste Anstieg von Niveau zu Niveau zu erkennen. Zum anderen wirkt die im Diagramm implizierte Entwicklung in Bezug auf die Genus-Bedingung robuster, da der C1-Mittelwert und dessen Konfidenzintervall (Fehlerbalken) in der Genus-Bedingung nicht mit den Konfidenzintervallen der B1- und B2-Daten überlappen. Wir können also mit einer relativ geringen Irrtumswahrscheinlichkeit annehmen, dass die Mittelwerte für B1 und B2 tatsächlich (außerhalb der Stichprobe) unterhalb des Mittelwertes für C1 liegen. Diese Inferenz ist in den anderen Bedingungen nicht so einfach, da die Konfidenzintervalle sich hier substanziell überlappen.

Wir können mit den Fehlerbalken auch ein anderes Intervall abbilden, beispielsweise das Intervall des Standarsfehlers. Hierfür müssen wir die aes-Parameter in geom_errorbar() entsprechend ändern:

  • ymin = m - se
  • ymax = m + se

Die Wahl des Intervalls, das durch die Fehlerbalken gezeigt wird, hängt letztlich davon ab, wie die beobachteten Werte analysiert werden sollen.


Aufgabe 5

Recherchieren sie:

  • Wie kann man die Achsen eines Diagramms manuell beschriften?
  • Wie kann man Textinhalte (z.B. numerische Werte) in ein Diagramm integrieren?
  • Wie kann man einem Diagramm Titel und Subtitel hinzufügen?
  • Wie kann man eine manuelle Farbauswahl treffen, beispielsweise für den Parameter fill?

Versuchen sie, diese Schritte für das oben gezeigte Balkendiagramm umzusetzen. Für einige der Schritte gibt es mehr als eine Lösung.

Lösung (Beispiel)

  • Mit xlab() und ylab() können wir die Achsen manuell beschriften.

  • Mit geom_text() können wir Textinhalte in das Diagramm integrieren. Wir können hiermit zum Beispiel die numerischen Angaben zu den Mittelwerten integrieren, indem wir unter aes() die Angabe label = m machen.

  • Mit ggtitle() können wir Titel mit der Angabe label = und Subtitel mit der Angabe subtitle = hinzufügen.

  • Mit scale_fill_manual() können wir eine manuelle Farbauswahl für den Parameter fill angeben und den Titel der erzeugten Legende ändern.

    Nachfolgend eine beispielhafte Anwendung auf unser Balkendiagramm. Wir zeigen nur den oben beschriebenen Code.

Aufgabe 6

Erstellen Sie ein Balkendiagramm mit den folgenden Inhalten auf Grundlage der by-Item Analyse, die Sie in Aufgabe 4 durchgeführt haben:

  • Mittelwert des Anteils korrekter Antworten für jedes Sprachniveau
  • Gruppierung nach Bedingung
  • Abbilden des Intervalls des Standardfehlers durch Fehlerbalken

Lösung


7 Putting it all to use: Analyse der Reaktionszeitdaten

Nachdem wir uns vorangehend mit dem Antwortverhalten (richtig vs. falsch) der Versuchspersonen beschäftigt haben, wollen wir nun die Reaktionszeitdaten analysieren. Die Reaktionszeit bezeichnet im Rahmen dieses Experiments die Zeit, die eine Versuchsperson ab der Präsentation der möglichen Referenten für ihre Antwort benötigt. Uns interessiert hierbei erneut, wie die Variable mit dem Sprachstand der Versuchspersonen interagiert. Der zugrundeliegende Gedanke ist, dass eine kürzere Reaktionszeit auf einen stärker automatisierten Verarbeitungsprozess schließen lässt. Wir wollen also wissen, ob die Pronomenauflösung in Abhängigkeit des Sprachstands unterschiedlich automatisiert erfolgt.

In der nachfolgenden Analyse werden wir größtenteils auf die zuvor erklärten Verfahren zurückgreifen, um diese anhand weiterer Beispiele zu demonstrieren. Es wird daher keine ausführliche Erklärung jedes Arbeitsschrittes mehr erfolgen. Für neue Verfahren erfolgt allerdings weiterhin eine Erklärung.

7.1 Verteilung der Reaktonszeitdaten 1: Kennwerte

Um die Vergleichbarkeit der Reaktionszeiten zwischen den verschiedenen Sprachniveaus zu gewährleisten, wollen wir zunächst für jedes Sprachniveau die Verteilung der Daten untersuchen. Uns interessiert hierbei einerseits, ob für alle Niveaus eine Normalverteilung angenommen werden kann, und andererseits, ob wichtige Kennwerte zur Streuung der Messungen in den verschiedenen Niveaus vergleichbar ausgeprägt sind.

Da uns die Verteilung aller Beobachtungen innerhalb eines Niveaus interessiert, errechnen wir die relevanten Kennwerte auf Grundlage der direkten Beobachtungen, und nicht, wie vorangehend, auf Grundlage einer Aggregierung über Versuchspersonen. Wir sind hierzu fähig, da Reaktionszeiten inhärent verhältnisskaliert sind. Wir können also zum Beispiel einen Mittewert auf Grundlage einzelner Beobachtungen berechnen und müssen nicht notwendigerweise über eine “Zwischengröße” wie Versuchspersonen oder Items aggregieren, wie bei der kategorialen Variable correct (das ist an mancher Stelle aber trotzdem sinnvoll, s.u.!)

  • Nach Angabe der Gruppenvariable level in group_by() definieren wir in reframe() unsere Kennwerte. Da uns die Verteilung der Werte interessiert, nehmen wir eine umfangreichere deskriptive Analyse mit den folgenden Werten vor:
    • n = Anzahl an Beobachtungen pro Sprachniveau, ermittelt mit length().
    • range = Spannweite der Stichprobe, berechnet mit max() - min().
    • iqr = Interquartilsabstand, berechnet mit IQR().
    • md = Median, berechnet mit md().
    • m = Arithmetisches Mittel, berechnet mit m().
    • sd= Standardabweichung, berechnet mit sd().
    • var = Varianz, berechnet mit var().
    • skew = Schiefegrad der Verteilung, manuell berechnet.
    • excess = Wölbung der Verteilung, manuell berechnet.
    • se = Standardfehler, manuell berechnet.
    • ci = Wert für das 95%-Konfidenzintervall, manuell berechnet.
sim_prn_res %>% 
  select(subject, level, item, reaction_time) %>% 
  group_by(level)%>%
  reframe(n = length(level), 
          iqr = IQR(reaction_time), 
          range = max(reaction_time)- min(reaction_time), 
          md = median(reaction_time),
          m = mean(reaction_time), 
          sd = sd(reaction_time), 
          var = var(reaction_time), 
          skew = (1/n) * sum(((reaction_time-m)/sd)^3),
          excess = ((1/n)*sum(((reaction_time-m)/sd)^4))-3, 
          se = sd/sqrt(n), 
          ci = 1.96*se)
## # A tibble: 3 × 12
##   level     n   iqr range    md     m    sd   var   skew excess    se    ci
##   <chr> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>  <dbl>  <dbl> <dbl> <dbl>
## 1 B1      223  44.4  219.  663.  661.  34.2 1170. -0.136  0.398  2.29  4.49
## 2 B2      310  47.4  171.  607.  609.  34.2 1170.  0.123 -0.458  1.94  3.81
## 3 C1      309  48.3  190.  574.  573.  34.7 1202. -0.111 -0.209  1.97  3.87

Der Output dieser Befehlskette zeigt uns die Kennwerte für jedes Sprachniveau (= 12 Spalten, 3 Zeilen).

Was sagen uns die Werte über die Verteilung unserer Daten?

  • Median (md) und Mittelwert (m) liegen in allen drei Gruppen nah beieinander, was jeweils auf hinreichend normalverteilte Daten schließen lässt, da der Mittelwert gleichzeitig die Mitte der Stichprrobe darstellt.
  • Der Schiefegrad (skew) der Verteilung ist zudem in allen Bedingunen nahe 0. Die Verteilungen können also als hinreichend symmetrisch (ergo als hinreichend normalverteilt) betrachtet werden. Allerdings liegen in den Gruppen jeweils unterschiedliche Tendenzen vor: B1 und C1 weisen eine leicht negative Schiefe auf und sind demnach leicht rechtssteil bzw. linksschief (mehr hohe Werte). B2 hat demgegenüber eine leicht positive Schiefe, ist also leicht linkssteil bzw. rechtsschief (mehr niedrige Werte).
  • Die Wölbung ist ebenfalls in allen Bedingugen nahe 0 und kann daher als hinreichend mit der Normalverteilung übereinstimmend betrachtet werden. Es liegen aber erneut unterschiedliche Trends vor: B1 tendiert zur schmalgipfligkeit (mehr Ausreißerwerte), während B2 und C1 zur Flachgipfligkeit tendieren (weniger Ausreißerwerte).
  • Interquartilsabstand (iqr), Standardabweichung (sd) und Varianz (var) sind in allen drei Gruppen ähnlich ausgeprägt. Es kann also auf ein ähnliches Streuungsverhalten in allen Gruppen geschlossen werden.

7.2 Verteilung der Reaktionszeitdaten 2: Visuelle Marker

Einige der eben vollzogenen Schlüsse können intuitiver und nachvollziehbarer erfolgen, wenn wir die Verteilung der Werte in allen Sprachniveaus visualisieren. Wir demonstrieren nachfolgend mehrere Visualisierungsverfahren mit ggplot2, die uns Aufschluss über die Verteilung der Werte geben.

7.2.1 Boxdiagramm

Boxdiagramme sind eine effektive Methode zur holistischen Betrachtung einer Stichprobe. Sie geben Aufschluss über die Spannweite von Werten in verschiedenen Quartilen einer Verteilung. Wir können hiermit Beispielsweise die gesamte Wertespanne, den Interquartilsabstand sowie den Median visuell erfassen.

  • Wir rufen mit ggplot() eine ggplot-Umgebung auf.
    • Wir ordnen in aes() die Variable level der X-Achse und die Variable reaction_time der Y-Achse zu.
  • Der Befehl geom_boxplot() ruft ein Boxdiagramm auf, das die aes-Parameter aus ggplot() übernimmt
  • Wir verwenden unsere Standard-Ästhetik mit theme_bw()
#Datenselektion#
sim_prn_res %>%
  select(item, level, reaction_time)%>%
  #Visualisierung#
  ggplot(
    aes(x = level, 
        y = reaction_time))+
  geom_boxplot()+
  theme_bw()

Unser Output enthält Boxdiagramme über die Reaktionszeiten für jedes Sprachniveau.

Wie ist das Boxdiagramm zu lesen?

  • Die vertikalen Linien des Diagramms zeigen jeweils die gesamte Wertespanne in einer Gruppe. Das obere Ende zeigt also den höchsten in einer Gruppe vorliegenden Wert, das untere Ende zeigt den niedrigsten in einer Gruppe vorliegenden Wert.
  • Die mittleren Kästen zeigen die mittleren Quartile, also die mittleren 50% der Verteilung. Die Größe der Kästen zeigt uns somit den Interquartilsabstand.
  • Der Linienverlauf oberhalb des Kastens zeigt das obere Quartil der Stichprobe, der Linienverlauf unterhalb des Kastens zeigt das untere Quartil.
  • Die horizontale Linie zeigt den Median der Stichprobe.
  • Die Punkte ober- bzw. unterhalb des Diagramms zeigen Werte, die als Ausreißer definiert wurden, also einen vordefinierten Mindestabstand zum letzten vorangehend beobachteten Wert aufweisen.

Wir können den Informationsgehalt des Boxdiagramms erhöhren, indem wir die individuellen Datenpunkte unserer Stichprobe durch ein Punktdiagramm in die Darstellung integrieren. Hiermit wird der Umfang der Stichprobe deutlicher, deren Wertespanne durch das Boxdiagramm abgebildet wird. Wir gehen hierbei wiefolgt vor:

  • Wir schreiben zunächst denselben Code wie vorangehend, machen jedoch eine zusätzliche Angabe in geom_boxplot():
    • Da wir jeden Wert der Stichprobe durch einen Datenpunkt in einem Punktdiagramm zeigen wollen, unterbinden wir die Darstellung von Ausreißerwerten in geom_boxplot(), da einige Werte ansonsten doppelt gezeigt würden. Hierfür machen wir in geom_boxplot() die Angabe outlier.shape = NA.
  • Mit geom_jitter() erstellen wir ein Punktdiagramm, das eine zerstreute Darstellung der Datenpunkte erlaubt. Hiermit erreichen wir, dass nicht sämtliche Datenpunkte einer Gruppe exakt dieselbe X-Koordinate aufweisen, wodurch die Größe und Verteilung der Stichprobe anschaulicher wird.
    • Wir rufen aes() auf und definieren einen Farbparameter für unsere drei Sprachniveaus mit der Angabe color = level.
    • mit alpha = 0.3 reduzieren wir die Sättigung der Datenpunkte, damit die Boxdiagramme weiterhin gut sichtbar sind.
    • mit width = 0.22 definieren wir das Maß an Streuung entlang der X-Achse.
#Datenselektion#
sim_prn_res %>%
  select(item, level, reaction_time)%>%
  #Visualisierung#
  ggplot(
    aes(x = level, 
        y = reaction_time))+
  geom_boxplot(outlier.shape = NA)+
  geom_jitter(
    aes(color = level), 
    alpha = 0.3, 
    width = 0.22)+
  theme_bw()

Durch die Integration des Punktdiagramms erhält man eine bessere Vorstellung der Stichprobengröße, über die das Boxdiagramm allein nichts aussagt. In unserem Fall ist das unter anderem deshalb sinnvoll, um die Vergleichbarkeit der verschiedenen Stichproben zu demonstrieren.

Was sagt uns das Boxdiagramm über die Verteilung unserer Daten?

  • Unsere vorangehenden Berechnungen haben gezeigt, dass der Interquartilsabstand in allen Gruppen vergleichbar ausgeprägt ist. Diesem Befund entspricht die vergleichbare Größe der Kästen in den Boxdiagrammen.

  • Oberes und unteres Quartil einer Gruppe zeigen ein ungefähr gleich großes Werteintervall

  • Die Werteintervalle oberhalb und unterhalb des Medians sind ebenfalls ungefähr gleich groß.

    Wir sehen also insgesamt in jeder Gruppe ein ausgeprägtes Symmetrieverhalten und können unsere Stichproben daher als hinreichend normalverteilt und vergleichbar betrachten. Wir sehen jedoch auch die folgenden, leicht asymmetrischen Tendenzen:

  • In den B1- und C2-Daten liegt der Median jeweils etwas höher als die Mitte der mittleren Quartile. Bei Aufteilung der Stichprobe in zwei gleichgroße Hälften liegt also eine leichte, rechtsverschobene Asymmetrie vor. Dieser Befund entspricht der zuvor rechnerisch ermittelten, negativen Schiefe (skew < 0) in beiden Gruppen: Relativ zum Mittelwert liegen mehr höhere Werte als niedrigere Werte vor.

  • In den B2-Daten liegt der Median etwas niedriger als die Mitte der mittleren Quartile. Es liegt also eine leichte, linksverschobene Asymmetrie vor, die zuvor bereits durch die positive Schiefe (skew > 0) erkannt wurde: Relativ zum Mittelwert liegen mehr niedrigere Werte als höhere Werte vor.

Mit dem Boxdiagramm werden einige Marker zum Verteilugsverhalten visuell abgebildet, gleichzeitig haben wir aber noch sehr guten Zugang zur Ausprägung der untersuchten Variable (hier: Reaktionszeit). Aus dem Boxdiagramm können beispielsweise nachwievor Gruppenunterschiede in der Reaktionszeit deutlich abgelesen werden, beispielsweise durch die Position des Medians oder der mittleren Quantile. Die nachfolgenden Darstellungen erschweren das Ablesen entsprechender Tendenzen, machen das Verteilungsverhalten dafür aber noch transparenter.

7.2.2 Histogramm

Ein Histogramm betrachtet die Variable von Interesse auf der X-Achse und gibt das Vorkommen von Werten oder Werteintervallen dieser Variable auf der Y-Achse an. Wir erstellen ein Histogramm für die Variable reaction_time, gruppiert nach Sprachniveau, auf folgendem Weg:

  • wir rufen mit ggplot() eine ggplot-Umgebung auf
    • Wir definieren unter aes() diesmal nur die Variabe für die X-Achse, da das nachfolgend aufgerufene Histogramm seine eigene Y-Variable spezifiziert. Unsere X-Achsen-Variable ist reaction_time.
  • Mit geom_histogram() rufen wir ein Histogramm auf.
    • Die Angabe zu binwidth legt fest, wie eng oder weitgefasst die Werteintervalle auf der X-Achse sein sollen, deren Vorkommen im Histogramm gezählt wird. Je höher bzw. niedriger der Wert, desto gröber bzw. feiner die Segmentierung der X-Achse. Mit der Angabe binwidth = 5 entscheiden wir uns für einer relativ feine Segmentierung.
    • Wir rufen aes() auf und definieren darin zwei Farbparameter mit der Variable level, um das Histogramm jeder Gruppe farblich zu kennzeichnen. Mit fill = level werden die einzelnen Balken (bins) des Histogramms farbig gefüllt, mit color = level werden die Balken farbig umrandet.
    • Mit alpha = 0.1 definieren wir den Sättigungsgrad der Balkenfarbe.
  • Mit dem Befehl facet_wrap() erreichen wir eine separate Darstellung jedes Diagramms innerhalb derselben Abbildung. Das ist deshalb sinnvoll, da wir jede Stichprobe möglichst klar abbilden wollen, zwischen den Stichproben aber große Überlappungen der Reaktionszeitwerte vorliegen. Mit facet_wrap() wird für jede Ausprägung der angegebenen Variable (hier: level) eine eigene X-Achse generiert.
  • Wir verwenden mit theme_classic() ein neues Thema, das ein rasterfreies Koordinatensystem erstellt.
#Datenselektion#
sim_prn_res %>%
  select(item, level, reaction_time)%>%
  #Visualisierung#
  ggplot(
    aes(x = reaction_time))+
  geom_histogram(binwidth = 5, 
                 aes(fill = level, 
                     color = level), 
                 alpha = 0.1)+
  facet_wrap(~level)+
  theme_classic()

Der Output zeigt uns ein Histogramm mit absoluten Häufigkeiten von Reaktionszeitintervallen für jedes Sprachniveau. Wegen der Angabe in facet_wrap() liegen drei separate Panels mit eigenen X-Achsen vor.

Was sagt uns das Histogramm über die Verteilung unserer Daten?

  • Eine wichtige Information, die wir der Abbildung entnehmen können, ist die Annäherung an das glockenförmige Muster normalverteilter Daten in jeder Gruppe.

    Es sind zudem erneut Tendenzen erkennbar, die wir zuvor rechnerisch bestimmt haben:

  • Das Histogramm der B1-Daten hat gegenüber den anderen Gruppen ein geringfügig “breiteres Fundament”. Es liegen also mehr Werte außerhalb der zentralen Tendenz vor, als in den B2- und C1-Daten. Dieser Befund entspricht dem positiven Exzess (> 0 - schmalgipflig) in den B1-Daten und dem negativen Exzess (< 0 flachgipflig) in den B2- und C1-Daten, den wir zuvor berechnet haben.

Mit dem Histogramm fokussieren wir bereits stärker das reine Verteilungsverhalten der Daten als mit dem Boxdiagramm, haben aber noch einen gewissen Zugang zur Ausprägung der abhängigen Variable, beispielsweise durch die ggf. unterschiedlichen Wertebereiche der X-Achsen, oder durch die X-Achsen-Position der höchsten Fallzahlen in jeder Gruppe. Das Histogramm stellt außerdem noch eine relativ authentische Aufbereitung der Daten da, da eine diskrete Häufigkeitsverteilung gezeigt wird, wie sie auch in unseren begrenzten Stichproben vorliegt.

7.2.3 Dichtegraph

Ein Dichtegraph zeigt den Kurvenverlauf einer geschätzten Wahrscheinlichkeitsdichtefunktion auf Grundlage einer gegebenen Stichprobe. Anders als das Histogramm, das eine diskrete Verteilung zeigt, zeigt uns der Dichtegraph also, wie eine Stetige Verteilung der untersuchten Variable auf Grundlage unserer Stichprobe aussähe.

Ein Dichtegraph kann zudem leicht mit der visuellen Darstellung der Normalverteilung verglichen werden. Wir können uns diesen Umstand zunutze machen, indem wir den Graph der Normalverteilung in unser Diagramm integrieren und direkt mit den Graphen unserer Stichproben vergleichen. Hierzu müssen die Werte unserer Stichproben aber zuerst auf die z-Skala transformiert werden, sodass wir über die notwendigen Referenzwerte verfügen, um eine Normalverteilung darzustellen.

  • Wir geben zunächst level als Gruppenvariable an, sodass die anschließende z-Transformation für jedes Sprachniveau separat erfolgt.
  • Wir verwenden mutate(), um die neue Variable z_rt zu definieren und diese dem nachfolgenden Code zuzuordnen.
    • Der Befehl scale() transformiert die Werte seines Arguments - in unserem Fall die Variable reaction_time - auf die z-Skala.
sim_prn_res %>%
  select(item, level, reaction_time)%>%
  group_by(level)%>%
  mutate(z_rt = scale(reaction_time))
## # A tibble: 842 × 4
## # Groups:   level [3]
##    item  level reaction_time z_rt[,1]
##    <fct> <chr>         <dbl>    <dbl>
##  1 1     B2             650.   1.19  
##  2 2     B2             610.   0.0210
##  3 4     B2             599.  -0.301 
##  4 5     B2             571.  -1.13  
##  5 6     B2             605.  -0.118 
##  6 7     B2             588.  -0.622 
##  7 8     B2             626.   0.495 
##  8 9     B2             619.   0.288 
##  9 11    B2             619.   0.270 
## 10 12    B2             618.   0.265 
## # ℹ 832 more rows

Der Output der Befehlskette enthält eine Variable z_rt, die jedem Wert der Variable reaction_time in einem bestimmten Sprachniveau einen entsprechenden z-Wert zuweist.

Auf Grundlage der z-Werte erstellen wir nun einen Dichtegraph der Reaktionszeiten für jedes Sprachniveau. Das nachfolgend beschriebene verfahren ist im Code ab #Visualisierung - Parameter# zu sehen.

  • Wir rufen mit ggplot() eine ggplot-Umgebung auf
    • Genau wie für das Histogramm geben wir auch beim Dichtegraph in aes() nur die X-Achsen-Variable an (hier reaction_time), da auch der Dichtegraph eine eigene Y-Achsen-Variable nutzt.
  • Mit geom_density() erstellen wir einen Dichtegraph.
    • Wir rufen aes() auf und spezifizieren die Farbparameter fill (= Farbfüllung) und color (= Kontur) nach der Variable level, so wie vorangehend auch beim Histogramm.
    • Mit der Angabe lwd = 0.8 passen wir die Breite der Verlaufslinie geringfügig an.
    • Mit alpha = 0.1 bestimmen wir die Farbsättigung des Dichtegraphs.
  • Mit facet_wrap() bewirken wir erneut, dass die Daten jedes Sprachniveaus in einem separaten Panel gezeigt werden.
  • Der Befehl stat_function() erlaubt uns, den Graphen einer von uns spezifizierten Funktion zu zeichnen.
    • Mit der Angabe fun = dnorm geben wir an, dass es sich um die Dichtefunktion der Normalverteilung handeln soll.
    • Mit der Angabe args = list (mean = 0, sd = 1) spezifizieren wir eine Normalverteilung auf der z-Skala (Standardnormalverteilung: Mittelwert = 0; Standardabweichung = 1).
    • Mit linetype = ‘dashed’ bestimmen wir den Linientyp für den Graph der Standardnormalverteilung (= gestrichelt).
sim_prn_res %>%
  select(item, level, reaction_time)%>%
  group_by(level)%>%
  mutate(z_rt = scale(reaction_time))%>%
  #Visualisierung#
  ggplot(
    aes(x = z_rt))+
  geom_density(
    aes(fill = level, 
        color = level), 
    lwd = 0.8, 
    alpha = 0.1)+
  facet_wrap(~level)+
  stat_function(fun = dnorm, 
                args = list(mean = 0, 
                            sd = 1), 
                linetype = 'dashed')+
  theme_classic()

Die resultierende Darstellung ähnelt in ihrem Aufbau dem zuvor erstellen Histogramm: Für jedes Sprachniveau wird ein eigener Dichtegraph der z-skalierten Reaktionszeiten gezeigt, der jeweils von der Kurve der Standardnormalverteilung überlagert wird. Zu beachten ist, dass die Y-Achse jetzt Wahrscheinlichkeiten, und keine absoluten Häufigkeiten zeigt. Außerdem zeigt die X-Achse nun die z-Skala anstelle der ursprünglichen Reaktionszeitwerte.

Was sagt uns der Dichtegraph über die Verteilung unserer Daten?

  • In allen drei Sprachniveaus liegt eine hohe Kongruenz der Verlaufslinien des Dichtegraphs und des Graphs der Standardnormalverteilung vor. Aus dieser Darstellung geht daher am deutlichsten hervor, dass die Reaktionszeitdaten hinreichend normalverteilt sind.

    Im direkten Vergleich mit dem graph der Standardnormalverteilung werden auch zuvor entdeckte, abweichende Tendenzen am deutlichsten sichtbar:

  • So ist die Linksschiefe der B1- und C1-Daten erkennbar: Beide Graphen zeigen in Werteintervallen links des Mittelwerts (= 0) eine niedrigere Wahrscheinlichkeit als der Graph der Standardnormalverteilung. Zudem ist in den B1-Daten gut sichtbar, dass die Spitze der Dichtefunktion (höchste Wahrscheinlichkeit) rechts vom Mittelwert (= 0) liegt, dass also relativ zum Mittelwert mehr höhere als niedrigere Werte vorliegen.

  • Umgekehrtes, also Rechtsschiefe, ist in den B2-Daten zu erkennen: Hier zeigt der Dichtegraph für ein Werteintervall rechts des Mittelwerts eine niedrigere Wahrscheinlichkeit als die Standardnormalverteilung. Zudem ist erkennbar, dass die Spitze des Dichtegraphen links vom Mittelwert (= 0) liegt, dass also mehr niedrigere als höhere Werte vorliegen.

Mit dem Dichtegraph können wir den allgemeinen Trend im Verteilungsverhalten unserer Daten am intuitivsten nachvollziehen. Für die Überprüfung auf Normalverteilung ist zudem die Möglichkeit zur Integration einer entsprechenden Verlaufskurve, wie in unserem Fall, sehr hilfreich. Gleichzeitig stellt der Dichtegraph gegenüber dem Histogramm oder dem Boxdiagramm eine stärkere Abstraktion von der eigentlichen Stichprobe dar, da wir nicht mehr die eigentlichen Daten, sondern eine auf deren Grundlage erstellte Funktion betrachten. Wenn unsere Daten z-skaliert sind, haben wir zudem nur noch Zugang zum Verteilungsverhalten und nicht mehr zur Ausprägung der untersuchten Variable.

7.3 Gemittelte Reaktionszeit nach Sprachniveau

Nachdem wir uns nun ausgiebig mit dem Verteilungsverhalten der Reaktionszeitdaten beschäftigt haben, wollen wir nachfolgend Gruppenunterschiede in der Reaktionszeit untersuchen. Um die Reaktionszeit auf einen potenziellen Einfluss des Sprachniveaus hin zu untersuchen, stellen wir für jedes Item die gemittelten Reaktionszeiten in jeder Gruppe gegenüber. Wir berechnen also zunächst für jedes Item die gemittelte Reaktionszeit nach Sprachniveau. Wir verwenden diese Mittelwerte für eine anschließende Visualisierung.

7.3.1 Berechnung der Kennwerte

Für das später erstellte Diagramm benötigen wir sowohl die einzelnen Beobachtungen aus unserem Dataframe sim_prn_res als auch die nachfolgend berechneten Kennwerte. Wir erstellen aus unseren nachfolgenden Berechnungen daher einen zusätzlichen Dataframe, den wir rt_means_item nennen. Das geschieht, wie zuvor auch, durch die Zuordnung des gewählten Objektnamens zum entsprechenden Code mit dem Zuordnungssymbol <-.

  • Wir geben in group_by() die Variablen item und level als Gruppenvariablen an.
  • Mit reframe() berechnen wir für jede Kombination von Item und Sprachniveau die folgenden Kennwerte:
    • n = Anzahl an Beobachtungen pro Item in jedem Sprachniveau
    • mean_rt = Mittelwert der Reaktionszeit pro Item in jedem Sprachniveau
    • sd_rt = Standardabweichung vom oben brerechneten Mittelwert
    • se = Standardfehler des Mittelwerts
    • t = t-Wert für die Berechnung des 95%-Konfidenzintervalls
    • ci = Wert des 95%-Konfidenzintervalls
rt_means_item <- 
  sim_prn_res %>%
  select(item, level, reaction_time)%>%
  group_by(item, level)%>% 
  reframe(n = length(item), 
          mean_rt = mean(reaction_time), 
          sd_rt = sd(reaction_time), 
          se = sd_rt/sqrt(n), 
          t = round(qt(0.975, df = n-1),2),  
          ci = t*se)

#Anschließende Betrachtung des neuen Dataframes#

head(rt_means_item)
## # A tibble: 6 × 8
##   item  level     n mean_rt sd_rt    se     t    ci
##   <fct> <chr> <int>   <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 1     B1        8    669.  33.9 12.0   2.36  28.3
## 2 1     B2       11    629.  33.8 10.2   2.23  22.7
## 3 1     C1       10    571.  24.5  7.75  2.26  17.5
## 4 2     B1        8    646.  30.2 10.7   2.36  25.2
## 5 2     B2       10    618.  21.6  6.83  2.26  15.4
## 6 2     C1       10    573.  24.0  7.59  2.26  17.2

Der neue Dataframe rt_means_item verfügt über 3 Zeilen für jedes Item, die jeweils die Kennwerte für das Item in einem der drei Sprachniveaus wiedergeben.

7.3.2 Visualisierung: Liniendiagramm für Mittelwerte + Punktdiagramm für Beobachtungen

Wir wollen nun die eben berechneten Mittelwerte für jedes Item visualisieren, um mögliche Gruppenunterschiede in der Reaktionszeit sichtbar zu machen. Wir verwenden ein gruppiertes Liniendiagramm, das die einzelnen Datenpunkte, in unserem Fall also die Mittelwerte unterschiedlicher Items im selben Sprachniveau, durch eine Linie verbindet. Hierdurch können wir sehen, ob es über alle Items hinweg einen konstanten Trend zu Gruppenunterschieden in der Reaktionszeit gibt, ob also die Linie für die B1-Daten konstant über der Linie der B2- oder C1-Daten liegt.

Um das Diagramm transparenter zu machen, wollen wir auch die einzelnen Beobachtungen für jedes Item, ebenfalls gruppiert nach Sprachniveau, in das Diagramm integrieren. Wir benötigen hierfür unseren ersten Dataframe sim_prn_res als Datengrundlage, um ein eintsprechendes Punktediagramm zu erstellen.

Das von uns angedachte Diagramm benötigt also mehrere Datengrundlagen und kann nicht, wie in den vorherigen Fällen, einfach an eine bestimmte Befehlskette angehängt werden, da ansonsten nicht alle notwendigen Daten verfügbar sind. Wir erstellen unser Diagramm diesmal also isoliert und spezifizieren die Inputdaten manuell.

  • Wir rufen mit ggplot() eine ggplot-Umgebung auf. Wir bestimmen die Parameter des Diagramms diesmal aber nicht global, wie in den vorherigen Fällen, da wir diesmal mehrere Variablen aus verschiedenen Dataframes auf dieselbe Achse projizieren wollen. Wir müssen die Parameter daher für jedes Diagramm separat bestimmen.
  • Wir rufen mit geom_point() ein Punktdiagramm auf.
    • Mit der Angabe data = sim_prn_res spezifizieren wir den gewünschten Input-Dataframe (sim_prn_res).
    • Wir verwenden aes() und ordnen die Variable item der X-Achse und die Variable reaction_time der Y-Achse zu. Mit der Angabe group = level nehmen wir eine Gruppierung nach Sprachniveau vor, die durch die Angabe color = level farblich gekennzeichnet wird.
    • Mit size = 2.5 bestimmen wir die Größe der Datenpunkte, mit alpha = 0.1 bestimmen wir die Farbsättigung.
  • Mit geom_line() erstellen wir ein Liniendiagramm.
    • Mit der Angabe data = rt_means_item geben wir unseren neuen Dataframe mit den Mittelwerten als Input an.
    • In aes() ordnen wir die Variable item der X-Achse und die Variable mean_rt der Y-Achse zu. Wir nehmen dieselbe Gruppierung und Farbkodierung vor wie im Punktdiagramm, sodass alle drei Sprachniveaus farblich gekennzeichnet werden.
    • Mit size = 1.5 bestimmen wir die Größe der Linien.
  • Mit geom_ribbon() integrieren wir das 95%-Konfidenzintervall für den Mittelwert in unser Darstellung.
    • Wir geben mit data = rt_means_item erneut unsere neuen Dataframe als Input an.
    • Wir verwenden aes() und definieren die untere Grenze des Intervalls (ymin) als die Differenz meanrt - ci und die obere Grenze (ymax) als die Summe mean_rt + ci. Wir nehmen erneut dieselbe Gruppierung und Farbkodierung nach Sprachniveau vor.
    • Wir bestimmen die Farbsättigung des Intervalls mit alpha = 0.1.
  • Mit theme_bw() verwenden wir wieder unser altes Thema mit Rasterlinien.

ggplot()+
  geom_point(data = sim_prn_res, 
             aes(x = item, 
                 y = reaction_time, 
                 group = level, 
                 color = level), 
             size = 2.5, 
             alpha = 0.5)+
  geom_line(data = rt_means_item, 
            aes(x = item, 
                y = mean_rt,
                group = level, 
                color = level), 
            size = 1.5)+
  geom_ribbon(data = rt_means_item, 
              aes(x = item, 
                  ymin = mean_rt - ci, 
                  ymax = mean_rt + ci, 
                  group = level, 
                  fill = level), 
              alpha = 0.1)+
  theme_bw()

Das resultierende Diagramm zeigt jedes unserer 30 Items auf der X-Achse. Die einzelnen Reaktionszeitbeobachtungen eines Items werden, gruppiert nach Sprachniveau, als Punkte auf der Y-Achse gezeigt. Die Linien zeigen, ebenfalls gruppiert nach Sprachniveau, die gemittelte Reaktionszeit nach Item. Der leicht eingefärbte Bereich um eine Linie zeigt jeweils das 95%-Konfidenzintervall für einen gegebenen Mittelwert.

Das Diagramm zeigt einen grundsätzlichen Trend zur Verringerung der Reaktionszeit mit steigendem Sprachniveau. Dieser Trend scheint in der Progression von B1 zu B2 insgesamt stärker ausgeprägt zu sein als in der Progression von B2 zu C1.

7.4 Gemittelte Differenz der Reaktionszeiten zwischen Sprachniveaus

Im vorangehenden Abschnitt haben wir einen Trend zur Verringerung der Reaktionszeit in Abhängigkeit des Sprachniveaus festgestellt. Unser Diagramm impliziert, dass die Verringerung der Reaktionszeit vom Niveau B1 zum Niveau B2 stärker ausgeprägt ist, als vom Niveau B2 zum Niveau C1. In unserem letzten Analyseschritt wollen wir diesen Umstand genauer untersuchen.

Unser Ziel ist, die Differenz der Mittelwerte zwischen Sprachniveaus für jedes Item zu berechnen. Wir berechnen also Beispielsweise für Item 1 die Differenz der gemittelten Reaktionszeit zwischen B1 und B2. Diesen Schritt führen wir dann für jedes Item aus. Dasselbe machen wir für die Differenz zwischen B2 und C1.

Um die angedachte Operation durchzuführen, müssen wir den Dataframe rt_means_item umgestalten, sodass die gemittelten Reaktionszeiten voneinander subtrahiert werden können. Aktuell befinden sich die Reaktionszeitwerte alle in derselben Spalte mean_rt, unabhängig ihrer Gruppenzugehörigkeit. Wir haben somit keine, oder nur sehr umständliche Möglichkeiten, einzelne Werte voneinander zu subtrahieren. Wir müssen daher zunächst ein Format erstellen, in dem eigene Spalten für die Reaktionszeiten für jedes Sprachniveau existieren, sodass wir anschließend eine Spalte von der anderen subtrahieren können.

  • Wir wählen zunächst mit select() die Variablen level, item und mean_rt aus. Anders als zuvor ist dieser Schritt diesmal sehr wichtig! Zu veranschaulichung betrachten wir kurz unsere reduzierte Datenstruktur.
rt_means_item %>%
    select(level, item, mean_rt)
## # A tibble: 90 × 3
##    level item  mean_rt
##    <chr> <fct>   <dbl>
##  1 B1    1        669.
##  2 B2    1        629.
##  3 C1    1        571.
##  4 B1    2        646.
##  5 B2    2        618.
##  6 C1    2        573.
##  7 B1    3        673.
##  8 B2    3        613.
##  9 C1    3        566.
## 10 B1    4        637.
## # ℹ 80 more rows

Für jedes Item liegen drei Zeilen vor, die jeweils die gemittelte Reaktionszeit für jedes Sprachniveau enthalten.

  • Mit pivot_wider() gestalten wir unseren Dataframe folgendermaßen um:
    • Mit der Angabe names_from = level geben wir an, dass die einzelnen Ausprägungen der Variable level - also B1, B2 und C1 - jeweils selbst als Variablen gelten sollen, sodass drei neue Spalten B1, B2 und C1 entstehen.
    • Mit der Angabe values from = mean_rt geben wir an, dass diejenigen Reaktionszeitwerte, die sich zuvor in derselben Zeile wie die Ausprägungen B1, B2 und C1 befanden, nun jeweils die Werte der neuen Variablen bilden. Die neue Variable B1 enthält also zum Beispiel alle Werte der Variable mean_rt, die zuvor in derselben Zeile wie die Ausprägung B1 der Variable level lagen.
rt_means_item %>%
    select(level, item, mean_rt)%>%
  pivot_wider(names_from = level, 
              values_from = mean_rt)
## # A tibble: 30 × 4
##    item     B1    B2    C1
##    <fct> <dbl> <dbl> <dbl>
##  1 1      669.  629.  571.
##  2 2      646.  618.  573.
##  3 3      673.  613.  566.
##  4 4      637.  604.  580.
##  5 5      651.  581.  573.
##  6 6      669.  620.  563.
##  7 7      675.  592.  574.
##  8 8      670.  619.  561.
##  9 9      644.  596.  565.
## 10 10     653.  614.  580.
## # ℹ 20 more rows

Wie der Name des Befehls nahelegt, erstellen wir mit pivot_wider() aus einer gegebenen Datenstruktur eine breitere (mehr Spalten) und kürzere (weniger Zeilen) Datenstruktur.

  • Wir verwenden nun mutate() um neue Spalten für die Differenz der Mittelwerte zwischen den verschiedenen Sprachniveaus zu generieren.
    • Die Differenz zwischen B1 und B2 nennen wir d_B1B2. Wir ordnen den Namen der Rechnung B1 - B2 zu.
    • Die Differenz zwischen B2 und C1 nennen wir d_B2C1. Wir ordnen den Namen der Rechung B2 - C1 zu.
    • Wir berechnen zusätzlich die Differenz zwischen den beiden vorangehenden Differenzen. Wir subtrahieren also die Werte aus d_B2C1 von den Werten aus d_B1B2. Das Ergebnis der Rechung nennen wir d_d (Differenz der Differenzen).
rt_means_item %>%
    select(level, item, mean_rt)%>%
    pivot_wider(names_from = level, 
                values_from = mean_rt) %>% 
    mutate(d_B1B2 = B1-B2, 
           d_B2C1 = B2-C1,
           d_d = d_B1B2 - d_B2C1)
## # A tibble: 30 × 7
##    item     B1    B2    C1 d_B1B2 d_B2C1    d_d
##    <fct> <dbl> <dbl> <dbl>  <dbl>  <dbl>  <dbl>
##  1 1      669.  629.  571.   40.9  57.9  -17.0 
##  2 2      646.  618.  573.   27.7  44.9  -17.2 
##  3 3      673.  613.  566.   59.9  46.7   13.1 
##  4 4      637.  604.  580.   33.6  23.8    9.79
##  5 5      651.  581.  573.   70.2   7.74  62.5 
##  6 6      669.  620.  563.   49.6  56.5   -6.96
##  7 7      675.  592.  574.   83.8  17.7   66.1 
##  8 8      670.  619.  561.   50.9  57.5   -6.53
##  9 9      644.  596.  565.   47.8  30.9   16.9 
## 10 10     653.  614.  580.   38.5  34.8    3.76
## # ℹ 20 more rows

Die neuen Variablen d_B1B2, d_B2C1 und d_d liegen nun in userer Datenstruktur vor. Wir testen nun unsere vorangehende Intuition, dass die Verringerung der Reaktionszeit von B1 zu B2 stärker ausgeprägt ist, als von B2 zu C1. Hierfür erstellen wir ein Liniendiagramm, das für jedes Item den Wert der Variable d_d zeigt. Sofern wir mit unserer Vermutung richtig liegen, sollte sich die Linie überwiegen oberhalb des Nullpunkts der Y-Achse befinden, da bei einer größeren Differenz B1-B2 gegenüber B2-C1 das Ergebnis der Subtraktion (B1-B2) - (B2-C1) positiv sein sollte. Das nachfolgend beschriebene Verfahren ist im unten gezeigten Code ab #Visualisierung# zu sehen.

  • Wir rufen mitt ggplot() eine ggplot-Umgebung auf.
    • Wir definieren in aes() die X-Variable item und die Y-Variable d_d. Wir machen zudem die Angabe group = 1. Hiermit geben wir an, dass lediglich eine Gruppe abgebildet werden soll.
  • Wir rufen mit geom_line() ein Liniendiagramm auf.
  • Wir rufen mit geom_point() ein Punktdiagramm auf, um die tatsächlichen Datenpunkte auf der Linie sichtbarer zu machen.
  • Mit geom_hline() integrieren wir eine Referenzlinie am Nullpunkt der Y-Achse. Den entsprechenden Y-Achsenabschnitt definieren wir mit der Angabe yintercept = 0. Mit color = ‘red’ bestimmen wir die Farbe der Linie.
rt_means_item %>%
  select(level, item, mean_rt)%>%
  pivot_wider(names_from = level, 
              values_from = mean_rt)%>%
  mutate(d_B1B2 = B1-B2, 
         d_B2C1 = B2-C1,
         d_d = d_B1B2 - d_B2C1 ) %>% 
  #Vislualisierung#
  ggplot(
    aes(x = item, 
        y = d_d, 
        group = 1))+
  geom_line()+
  geom_point()+
  geom_hline(yintercept = 0, 
             color = 'red')+
  theme_bw()

Unsere Vermutung bestätigt sich: Die Linie befindet sich mehrheitlich über dem Nullpunkt, die Differenz B1 - B2 ist demnach für die Mehrzahl an Items größer als die Differenz B2 - C1. Wir sehen zudem, dass in den Fällen, in denen das Gegenteil vorliegt, die Differenz B2 - C1 nur geringfügig größer ist, als die Differenz B1 - B2, da das Liniendiagramm den Nullpunkt der Y-Achse nur geringfügig unterschreitet. Demgegenüber liegen für die Items mit positiver Differenz stärkere Überschreitungen des Nullpunkts vor.

Alternativ zu einer visuellen Untersuchung können wir unsere Vermutung auch einfach durch die Berechnung von Mittelwerten der Differenz zwischen den verschiedenen Sprachniveaus überprüfen. Zuvor überführen wir unseren Dataframe aber wieder in sein ursprüngliches Format. Das nachfolgend beschriebene Verfahren ist im unten gezeigten Code ab #Rückführung in langes Format# zu sehen.

  • Mit select() wählen wir die Variablen aus, die wir nachfolgend in eine neue Datenstruktur transformieren wollen. Diese sind item, d_B1B2 und dB2C1.
  • Mit pivot_longer() gestalten wir unseren Dataframe folgendermaßen um:
    • Das Argument cols = gibt an, welche unserer aktuellen Spalten transformiert werden sollen. Mit cols = !item geben wir daher an, dass alle Variablen außer item umgestaltet werden sollen. Zur Erinnerung: Das Ausrufezeichen fungiert als Negationsoperator.
    • Mit names_to = ‘comparison’ definieren wir eine neue Variable (comparison), deren Ausprägungen die aktuellen Spaltennamen d_B1B2 und dB2C1 sein werden.
    • Mit values to ‘difference’ definieren wir eine neue Variable, deren Ausprägungen die Werte sein werden, die aktuell unter den Spalten d_B1B2 und dB2C1 stehen.
rt_means_item %>%
  select(level, item, mean_rt)%>%
  pivot_wider(names_from = level, 
              values_from = mean_rt)%>%
  mutate(d_B1B2 = B1-B2, 
         d_B2C1 = B2-C1,
         d_d = d_B1B2 - d_B2C1 ) %>% 
  select(item, d_B1B2, d_B2C1) %>% 
  #Rückführung in langes Format#
  pivot_longer(cols = !item,
               names_to = 'comparison',
               values_to = 'difference')
## # A tibble: 60 × 3
##    item  comparison difference
##    <fct> <chr>           <dbl>
##  1 1     d_B1B2          40.9 
##  2 1     d_B2C1          57.9 
##  3 2     d_B1B2          27.7 
##  4 2     d_B2C1          44.9 
##  5 3     d_B1B2          59.9 
##  6 3     d_B2C1          46.7 
##  7 4     d_B1B2          33.6 
##  8 4     d_B2C1          23.8 
##  9 5     d_B1B2          70.2 
## 10 5     d_B2C1           7.74
## # ℹ 50 more rows

Als Output erhalten wir einen längeren (mehr Zeilen) und “engeren” (weniger Spalten) Dataframe, wie wir ihn aus unserer bisherigen Arbeit gewohnt sind. Beispielsweise liegen wieder zwei Zeilen pro Item vor, die jeweils eine Angabe zum Vergleichsmoment (comparison) und zur entsprechenden Differenz (difference) enthalten.

Mit diesem Datafarme berechnen wir nun die Mittelwerte der Differenz für jedes Vergleichsmoment (im Code ab #Kennwerte#).

  • Wir in group_by() die Variable comparison als Gruppenvariable an.
  • Wir berechnen in reframe() die folgenden Kennwerte für jedes Vergleichsmoment-
    • n = Anzahl an Beobachtungen für jedes Vergleichsmoment
    • m = Mittelwert der Differenz zwischen den Sprachniveaus
    • sd = Standardabweichung vom Mittelwert
rt_means_item %>%
  select(level, item, mean_rt)%>%
  pivot_wider(names_from = level, 
              values_from = mean_rt)%>%
  mutate(d_B1B2 = B1-B2, 
         d_B2C1 = B2-C1,
         d_d = d_B1B2 - d_B2C1 ) %>% 
  select(item, d_B1B2, d_B2C1) %>% 
  pivot_longer(cols = !item,
               names_to = 'comparison',
               values_to = 'difference') %>% 
  #Kennwerte#
  group_by(comparison) %>% 
  reframe(n = length(comparison), 
          m = mean(difference), 
          sd = sd (difference))
## # A tibble: 2 × 4
##   comparison     n     m    sd
##   <chr>      <int> <dbl> <dbl>
## 1 d_B1B2        30  52.0  14.9
## 2 d_B2C1        30  36.3  14.6

Das Ergebnis bestätigt erneu unsere Intuition, dass die Reaktionszeit in der Progression von B1 zu B2 stärker verringert wird, als in der Progression von B2 zu C1.


Aufgabe 7 (zum Grübeln)

Gegeben sei das folgende Szenario:

  • Wir möchten zeigen, dass die Variable level, die jede Versuchsperson auf Grundage ihres Testscores in eine von drei Kategorien (B1, B2 und C1) einteilt, nicht willkürlich ist. Es soll also gezeigt werden, dass diese Einteilung mit der Ausprägung weiterer Variablen (neben test_score) unseres Datensatzes korreliert.

Wie können wir vorgehen, um einen entsprechenden Nachweis für zwei Variablen gleichzeitig zu erbringen?

Lösung

  • Wir führen eine by-Subject Analyse durch, in der wir für jede Versuchsperson die folgenden Kennwerte erheben:

    • den Anteil korrekter Antworten p
    • die gemittelte Reaktionszeit mean_rt

  • Wir geben die Variable level als zusätzliche Gruppenvariable an, um später auf diese zugreifen zu können.

  • Wir erstellen ein Punktdiagramm, das p auf der Y-Achse und mean_rt auf der X-Achse zeigt. Zusätzlich geben wir den Farbparameter color = level an, sodass ein gegebener Datenpunkt farblich für sein zugehöriges Sprachniveau markiert wird.

  • Wir sehen, dass die Cluster, die bei der gemeinsamen Betrachtung beider Variablen entstehen, exakt von unserer kategorischen Variable level erfasst werden.

Aufgabe 8

Kopieren Sie den unten angezeigten Code in Ihr Skript und führen Sie Ihn aus.

subject_1 <- rnorm(30, mean = 600, sd = 15)
subject_2 <- rnorm(30, mean = 600, sd = 15)
subject_3 <- rnorm(30, mean = 600, sd = 15)
subject_4 <- rnorm(30, mean = 600, sd = 15)
subject_5 <- rnorm(30, mean = 600, sd = 15)
subject_6 <- rnorm(30, mean = 600, sd = 15)
subject_7 <- rnorm(30, mean = 600, sd = 15)
subject_8 <- rnorm(30, mean = 600, sd = 15)
subject_9 <- rnorm(30, mean = 600, sd = 15)
subject_10<- rnorm(30, mean = 600, sd = 15)
                       
wide_data <-  data.frame(subject_1, subject_2, subject_3, subject_4, subject_5, subject_6, subject_7, subject_8, subject_9,subject_10)

answer <- sample(c(1,0), size = 300, replace = TRUE, prob = c(0.7,0.3))
item_long <- rep(c(1:30), each = 10)
test <- rep(1:30)
subject_long <- rep(test, 10)
condition_long <- sample(c(1,2), size = 300, replace = TRUE, prob = c(0.5, 0.5))

long_data <- data.frame(subject_long, item_long,condition_long, answer)

In Ihrem Workspace sollten Ihnen zwei neue Dataframes angezeigt werden: long_data und wide_data.

Beide Dataframes enthalten Daten derselben 10 Versuchspersonen:

  • long_data enthält eine subject id, eine item id, eine Angabe zur experimentellen Bedingung (1 vs. 2) und eine Angabe zum Antwortverhalten (1 vs. 0)
  • wide_data enthält für jede Versuchsperson Angaben zur Reaktionszeit in einem Trial aus long_data.

Wir möchten beide Dataframes mit dem untenstehenden Code kombinieren:

cbind(long_data, wide_data)

8.1

Bevor dieser Schritt ein Sinnvolles Ergebnis produziert, müssen wir die Struktur von wide_data allerdings auf bestimmte Weise verändern. Was müssen wir tun, und wie gehen wir dabei vor?

Wenn Sie bereits eine Idee haben, was zutun ist, hier ein praktischer Tipp

Die Angabe col = in pivot_longer() gibt an, welche Spalten des Datensatzes von der Transformation betroffen sein sollen. Im Falle von wide_data soll jede Spalte betroffen sein, da jede Spalte Daten einer Versuchsperson enthält. Es gibt hier jedoch einen einfacheren Weg, als den Namen jeder Spalte manuell anzugeben (also col = subject_1, subject_2 …). Wir können uns zunutze machen, dass alle Spaltennamen gleich beginnen. Recherchieren Sie!

Lösung

8.2

Nachdem wide_data entsprechend umgestaltet wurde, können wir nun beide Dataframes mit cbind() kombinieren. Wir nennen den resultierenden Dataframe long_wide.

long_wide <-  cbind(long_data, wide_data)

Dieser Dataframe enthält zwei Spalten zur subject id, beide Spalten enthalten also dieselbe Information. Entfernen Sie daher eine der beiden Spalten und überschreiben Sie den Dataframe.

Betrachten sie außerdem die Kodierung der experimentellen Bedingung in der Spalte condition_long. Welches Problem könnte auftreten, wenn wir versuchen, die Daten beider Bedingungen zu vergleichen? Wie vermeiden wir dieses Problem?

Lösung

  • Die Variable condition_long wird als numerische Variable kodiert. Für einen Vergleich der beiden experimentellen Bedingungen muss eine kategorische Variable vorliegen. Wir wandeln condition_long daher in einen Faktor um.

8.3.

Betrachten sie die Verteilung der Reaktionszeiten in beiden experimentellen Bedingungen in einem Boxdiagramm. Fügen sie außerdem ein Punktdiagramm ein, dass die zugrundeliegende Stichprobe abbildet.

Lösung


8 Objekte und Grafiken als Datei speichern

Manchmal ist es notwendig, Datenobjekte, die man mit R erstellt hat, als Datei abzuspeichern, zum Beispiel, wenn man Daten veröffentlicht. Dies kann ganz einfach mit dem Befehl „write.csv“ erfolgen.

Der Befehl wird unten anhand des Dataframes rt_means_item erstellt, der bisher nur als Objekt in unserem Workspace vorliegt. Mit row.names=FALSE wird verhindert, dass in der gespeicherten Datei ein Zeilenindex erscheint.

write.csv(rt_means_item, file="mean_reaction_time.csv", row.names=FALSE)

Um Grafiken als Bilddatei zu speichern, zum Beispiel als .jpg, kann der Befehl ggsave verwendet werden. Der folgende Code gibt an, dass die letzte erstellte Grafik als jpg-Datei gespeichert wird. Es können Größe und Auflösung geändert werden..

ggsave(filename = "Grafik.jpg", last_plot(), width = 9, height = 5, dpi = 300)

Alternativ kann eine Grafik auch über den Export-Reiter im rechten unteren Fenster im R-Studio-Interface exportiert werden.

Übrigens kann auch eine in R erstellte Grafik als Objekt definiert werden. Man geht hierbei genauso vor, wie beim erstellen anderer Objekte. Der Code, der die Grafik produziert, wird über das Zuweisungssymbol einem Objektnamen zugewiesen.


Generieren des Beispieldatensatzes

set.seed(1)

Items = 30
Subjects = 30

condition_c <-  rep(c('Genus', 'Semantik', 'Genus+Semantik'), each = (Items/3))
condition <-  rep(condition_c, 30)
subject <-  rep(c(1:Subjects), each = Items)
l1 <-  rep(sample(c('Spanisch', 'T?rkisch', 'Chinesisch', 'Englisch', 'Niederl?ndisch', 'Arabisch', 'Russisch', 'Italienisch',
                  'Kroatisch', 'Tschechisch'),replace = TRUE, size = 30, prob = c(0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1) ), each = Subjects)
age <-  round(rep(rnorm(30, mean = 25, sd = 3 ), each = Subjects))
item_c <-  rep(c(1:Items))
item <-  rep(item_c, 30)
test_score <-  rep(sample(c(60:100), size = Subjects), each = Subjects)

correct <-  case_when((test_score <= 73) & (condition == 'Genus') ~ sample(c(1,0,NA), replace = TRUE, size = 900, 
                                                                           prob = c(0.62, 0.33, 0.05)),
                    (test_score <= 73) & (condition == 'Semantik') ~ sample(c(1,0,NA), replace = TRUE, size = 900, 
                                                                            prob = c(0.74, 0.21, 0.05)),
                    (test_score <= 73) & (condition == 'Genus+Semantik') ~ sample(c(1,0,NA), replace = TRUE, size = 900, 
                                                                                  prob = c(0.75, 0.2, 0.05)),
                    (test_score > 73 & test_score <= 86) & (condition == 'Genus') ~ sample(c(1,0,NA), size = 900, replace = TRUE,
                                                                                           prob = c(0.76, 0.19, 0.05)),
                    (test_score > 73 & test_score <= 86) & (condition == 'Semantik') ~ sample(c(1,0,NA), size = 900, replace = TRUE,
                                                                                              prob = c(0.83, 0.12, 0.05)),
                    (test_score > 73 & test_score <= 86) & (condition == 'Genus+Semantik') ~ sample(c(1,0,NA), 
                                                                                                    size = 900, replace = TRUE, 
                                                                                                    prob = c(0.83, 0.12, 0.05)),
                    (test_score > 86) ~ sample(c(1,0,NA), replace = TRUE, size = 900, prob = c(0.9, 0.05, 0.05)))

reaction_time <-  case_when((test_score <= 73) & (correct == 1|correct == 0) ~ rnorm(900, mean = 665, sd = 35),
                          (test_score > 73 & test_score <= 86) & (correct == 1|correct == 0)  ~ rnorm(900, mean = 610, sd = 35),
                          (test_score > 86) & (correct == 1|correct == 0) ~ rnorm(900, mean = 570, sd = 35))


sim_prn_res <-  data.frame(subject, l1, age, test_score, item, condition, correct, reaction_time)