5 Aufarbeitung von Fragebogendaten

Unser Ziel ist die Auswertung echter Daten aus Fragebögen wie den BIG-5 und dem Narcissistic Personality Inventory. Allerdings liegen echte Daten oftmals nicht in der Form vor, die wir benötigen. Echte Daten haben fehlende Werte, und meistens müssen wichtige Informationen erst aus den bestehenden Variablen abgeleitet werden. Aus diesem Grund beschäftigen wir uns im vorliegenden Kapitel mit den folgenden Themen:

  1. Umkodierung von Antworten
  2. Invertierung von Antworten
  3. Umgang mit fehlenden Werten

5.1 Umkodierung von Antworten

Eine wichtige Voraussetzung für eine psychometrische Analyse war im Beispiel in Kapitel 4 bereits gegeben: Jeder Wert kodierte genau die Information, die wir brauchten – nämlich ob Schüler/innen eine Aufgabe korrekt gelöst hatten oder nicht (dargestellt durch 1 und 0). Bei echten Datenerhebungen wird im Normalfall aber nicht direkt Korrektheit kodiert, sondern diese wird aus der eigentlichen Antwort abgeleitet. Die Antwort könnte beispielsweise ein Kreuz in einem Multiple-Choice-Item sein:

Aus wie vielen Bundesländern besteht die Bundesrepublik Deutschland?

  1. 14
  2. 16
  3. 19
  4. 21

Bei der Dateneingabe des Tests kann zwischen 1 und 4 kodiert werden, welche der vier Antwortoptionen ausgewählt wurde. Ob ein Kreuz bei (1), (2), (3) oder (4) gesetzt wird, ist für die Datenauswertung aber nicht unbedingt von Belang. Es ist vor allem relevant, ob die Frage richtig beantwortet wurde – wir benötigen also die folgende Umkodierung der Daten:

  • 1 \(\to\) 0
  • 2 \(\to\) 1
  • 3 \(\to\) 0
  • 4 \(\to\) 0

In psychometrischem Jargon: Der Wert 2 ist der Schlüssel (engl.: key) dieser Aufgabe. Ein Schlüssel kodiert den Eingabewert der richtigen Antwort.27 Wir lernen jetzt, wie wir solche Umkodierungen in R umsetzen. Die Stärke einer Programmiersprache wie R: Wenn wir einmal gelernt haben, wie wir für eine Item-Schlüssel-Kombination Daten als richtig und falsch umkodieren, können wir mit nur ein wenig mehr Aufwand diesen Prozess für beliebig viele Items wiederholen. Das Narcissistic Personality Inventory etwa hat 40 Items und wir haben keine Lust, 40 Mal eine Umkodierung händisch neu durchzuführen.

5.1.1 Die Funktion ifelse()

Mit der Funktion ifelse() lassen sich Transformationen, die anhand eines Schlüssels Korrektheit kodieren, bequem durchführen. Das folgende Beispiel basiert auf dem obigen Multiple-Choice-Item:

[1] 0 1 0 0 1 0 1

Was ist hier passiert? Ich habe im Vektor bundesland_answers hypothetische Antworten generiert; die Variable bundesland_key enthält den Schlüssel, d.h. die korrekte Antwort. Mithilfe der Funktion ifelse() gleiche ich die Antworten mit dem Schlüssel ab. Sie nimmt drei Argumente entgegen: test, yes, und no.

  • test: Ein logischer Vektor – hier der Vergleich jeder Antwort mit dem Schlüssel
  • yes: Die Ausgabe an den Positionen, an denen der test TRUE ergibt
  • no: Die Ausgabe an den Positionen, an denen der test FALSE ergibt

In diesem Fall wird dem Argument test der folgende logische Vektor übergeben:

[1] FALSE  TRUE FALSE FALSE  TRUE FALSE  TRUE

An allen Positionen, an denen dieser Vergleich TRUE ergibt, gibt die Funktion ifelse() den Wert 1 aus – also den Wert, der dem Argument yes übergeben wurde. An allen anderen Positionen steht 0 – also der Wert, der dem Argument no übergeben wurde. Praktisch: Ich muss nicht angeben, welche falschen Werte alle möglich sind; es reicht aus, im test mit dem richtigen Wert zu vergleichen. Alle anderen Werten werden dann automatisch als falsch interpretiert.

Nach der Umkodierung können wir mit der Funktion mean() die Schwierigkeit des Items berechnen:

[1] 0.4285714

In diesem Fall hätten 43% der Testteilnehmer das Bundesland-Item korrekt beantwortet. Diese Information konnten wir aus den ursprünglichen Antwortkategorien 1, 2, 3 und 4 nicht herleiten. Auch um Item-Trennschärfen, Cronbachs Alpha oder andere Kennwerte zu bestimmen, ist die Umkodierung eine notwendige Voraussetzung.

5.1.2 Vektorisierung der Funktion ifelse()

Die Argumente yes und no der Funktion ifelse() sind im allgemeinen Fall „vektorisiert“, d.h. sie können dieselbe Länge haben wie das Argument test. Dann wird positionsweise entschieden, welche Elemente in der Ausgabe enthalten sind. Wenn beispielsweise test an der ersten Position TRUE ergibt, ist das erste Element Ausgabe das erste Element des Arguments yes. Wenn test an der zweiten Position FALSE ergibt, ist das zweite Element Ausgabe das zweite Element des Arguments no. Ein kurzes Beispiel ist durch den folgenden Code gegeben:

[1]  1 12

Diese Funktionalität ist vor allem dann wichtig, wenn wir in einem Vektor nur manche Elemente umkodieren wollen, während andere Elemente unverändert bleiben sollen. Etwa:

 [1] -1 -2 -3 -4 -5  6  7  8  9 10

Ein weiteres Beispiel:

 [1] -100 -100 -100 -100 -100    6    7    8    9   10

Frage: Was ist in diesem Fall der Unterschied zum folgenden Befehl:

Die Vektorisierung von ifelse() ist oftmals der Grund, warum man die Funktion überhaupt verwendet, für uns aber erst einmal nicht von Interesse. Wir nutzen ifelse() zunächst nur zur dichotomen Bepunktung von Items (durch Abgleich von Antworten mit einem Schlüssel).

5.2 Invertierung von Antworten

Eine mögliche Umkodierung von Antworten ist das Abgleichen mit einem Schlüssel, etwa zur Feststellung der Korrektheit von Antworten. Eine weitere häufig auftretende Variante ist die Invertierung von Antworten. Betrachten wir folgende zwei Items, die in einer Big-5 Kurzskala den Aspekt Extraversion messen:

  1. Ich bin eher zurückhaltend, reserviert.
  2. Ich gehe aus mir heraus, bin gesellig.

Beide Items werden auf einer Likertskala mit fünf Abstufungen gemessen, das heißt es werden Punktzahlen von 1 bis 5 vergeben. Das Problem ist, dass in Item 1 ein hoher Punktwert für wenig Extraversion steht, in Item 2 ein hoher Punktwert hingegen für eine hohe Ausprägung in Extraversion. Generell wollen wir einen Summenwert berechnen, also einen Wert, der die Extraversion eines jeden Testteilnehmers kodiert – und zwar über beide Items hinweg. Im vorliegenden Fall macht es aber keinen Sinn, die Punktzahlen beider Items zu addieren. Die höchste Ausprägung in Extraversion würde sich dann ergeben, wenn ein Item extravertiert beantwortet wird, aber das andere introvertiert. Damit die Punktzahlen in beiden Items „in dieselbe Richtung“ zu verstehen sind, wollen wir die Antworten auf Item 1 invertieren, sodass auch hier eine hohe Punktzahl für eine hohe Merkmalsausprägung in Extraversion steht. Das heißt, wir wollen die folgende Abbildung durchführen:

  • 1 \(\to\) 5
  • 2 \(\to\) 4
  • 3 \(\to\) 3
  • 4 \(\to\) 2
  • 5 \(\to\) 1

Wir könnten dies mit mehrfacher Anwendung von ifelse() hinbekommen, was jedoch mühsam wäre. Glücklicherweise gibt es zur Invertierung eine mathematische Umformung, die wir auch mit nur wenig Code umsetzen können:

Invertierter Wert = Ursprungswert * (-1) + Höchster Skalenwert + 1

Diese funktioniert, wenn unsere Punktzahlen zwischen 1 und dem höchstmöglichen Skalenwert liegen. Probieren wir es mit ein paar hypothetischen Antworten aus:

Wir können uns mit der cor() Funktion die Korrelation zwischen den zwei Items ausgeben lassen:

      item1 item2
item1  1.00 -0.57
item2 -0.57  1.00

Ich habe die Antwortwerte absichtlich so gewählt, dass sich hier ein typisches Muster ergibt: Antworten auf unterschiedlich gepolte Items – die zur selbem Skala gehören – korrelieren typischerweise negativ miteinander. Das heißt: Hohe Antwortwerte im einen Item gehen tendenziell mit niedrigen Werten im anderen Item einher – wenn die unterschiedlich gepolten Items dasselbe Konstrukt erfassen. Durch die Invertierung erhalten wir Daten, die positiv miteinander korrelieren. Folgender Code führt die Invertierung durch:

Schauen wir uns die Daten an, um zu prüfen, ob die Transformation funktioniert hat:

  item1 item1_inv
1     2         4
2     3         3
3     2         4
4     1         5
5     4         2
6     2         4
7     1         5
8     5         1

Das hat geklappt! Schauen wir uns nun auch noch einmal die Inter-Itemkorrelationen an:

          item1 item2 item1_inv
item1      1.00 -0.57     -1.00
item2     -0.57  1.00      0.57
item1_inv -1.00  0.57      1.00

Wie wir sehen, korrelieren die Spalten item2 und item1_inv genau wie item2 und item1 – nur mit positivem Vorzeichen. Ebenfalls interessant: item1 und item1_inv korrelieren perfekt negativ – und das ist genau das, was wir mit der Invertierung erreichen wollten: Einen Punktwert errechnen, der genau in die entgegengesetzte Richtung zu interpretieren ist wie der ursprüngliche Wert.

5.2.1 Invertierung mit ifelse()

Wie oben erwähnt, können wir auch durch mehrfachen Aufruf von ifelse() eine Item-Invertierung umsetzen. In dem Fall wird jede der nötigen Umformungen (1 \(\to\) 5; 2 \(\to\) 4; 3 \(\to\) 3; 4 \(\to\) 2; 5 \(\to\) 1) durch einen separaten Aufruf von ifelse() umgesetzt. Anstatt einmal die Invertierungsformel aufzurufen müssen wir also fünfmal ifelse() benutzen. Folgender Code zeigt, wie das funktionieren würde:

Auch hier können wir durch Korrelation zwischen dem ursprünglichen Item und dem invertiertem Item feststellen, ob die Invertierung erfolgreich war:

[1] -1

Bei mehrfacher Verwendung von ifelse() zur Umkodierung verschiedener Werte eines Vektors nutzen wir die Vektorisierung des Arguments no (wie oben erklärt). NA dient dabei als Platzhalter für die Elemente, die bislang noch nicht umkodiert wurden. Wenn keine mathematische Formulierung für eine Umkodierung existiert, ist es durchaus gängig, dass Umkodierungen genau in dieser Form per mehrfacher Verwendung von ifelse() umsetzt werden.

5.3 Umgang mit fehlenden Werten

Real data have missing values. Missing values are an integral part of the R language. Many functions have arguments that control how missing values are to be handled. – Patrick Burns28

Wir lernen nun den rudimentären Umgang mit fehlenden Werten in R kennen. Dabei könnte man vermutlich beliebig sophistiziert vorgehen, jedoch werden wir nur einen basalen und wichtigen Spezialfall kennenlernen:

  1. Wir wandeln alle Werte in NA um, die als fehlend zu klassifizieren sind
  2. Danach schließen wir alle Fälle mit fehlenden Werten aus

Für dieses Beispiel laden wir Daten des Narcissistic Personality Inventory (NPI; Raskin & Terry, 1988) ein. Die Daten von mehr als 11,000 Bearbeitungen des NPI sind erfreulicherweise über das „Open Source Psychometrics Project“ unter https://openpsychometrics.org/_rawdata abrufbar. Wenn wir die Daten heruntergeladen haben und die Datei „data.csv“ in unserem RStudio-Projektordner liegt (siehe Anhang), können wir den Datensatz wie folgt einlesen:

Wie folgt verschaffen wir uns einen Überblick über die Daten:

[1] 11243
[1] 44
  score Q1 Q2 Q3 Q4 Q5 Q6 Q7 Q8 Q9 Q10 Q11 Q12 Q13 Q14 Q15 Q16 Q17 Q18 Q19 Q20
1    18  2  2  2  2  1  2  1  2  2   2   1   1   2   1   1   1   2   1   1   1
2     6  2  2  2  1  2  2  1  2  1   1   2   2   2   1   2   2   1   1   2   1
3    27  1  2  2  1  2  1  2  1  2   2   2   1   1   1   1   1   2   2   1   1
  Q21 Q22 Q23 Q24 Q25 Q26 Q27 Q28 Q29 Q30 Q31 Q32 Q33 Q34 Q35 Q36 Q37 Q38 Q39
1   1   1   1   2   2   2   1   2   2   2   1   2   1   1   1   2   2   2   1
2   2   2   1   2   2   2   2   1   2   2   2   1   2   2   1   2   2   2   2
3   2   2   2   2   1   2   1   1   2   1   2   2   1   1   2   1   1   2   1
  Q40 elapse gender age
1   2    211      1  50
2   1    149      1  40
3   2    168      1  28

Wir bemerken, dass keine Variable als “Fallnummer” fungiert. Generell ist es immer wichtig, dass jeder Datensatz durch eine eindeutige Fallnummer zu identifizieren ist. Da eine solche in den eingelesenen Daten jedoch nicht enthalten ist, fügen wir selber eine Fallnummer hinzu:

5.3.1 Identifikation von fehlenden Werten

Das NPI besteht aus 40 Items. Aus dem Codebuch des NPI Datensatzes29 erfahren wir, dass Antworten auf die NPI Items die Werte 1 und 2 annehmen können. Die Antwort auf jedes Item des NPI besteht aus einer „forced choice“ zwischen zwei Aussagen; eine davon wird als Indikator für Narzissmus interpretiert. Item 1 besteht beispielsweise aus den folgenden beiden Aussagen:

  1. Ich habe eine natürliche Begabung, auf Menschen Einfluss zu nehmen.
  2. Ich kann nicht besonders gut Einfluss auf jemanden ausüben.

Die Wahl von Aussage 1 wird mit 1 kodiert, die Wahl von Aussage 2 mit 2. Nachgeschaltet wird folgende Umkodierung vorgenommen, die die Item-Scores berechnet: Wird die „narzisstische Aussage“ ausgewählt (hier Aussage 1: „Ich habe eine natürliche Begabung, auf Menschen Einfluss zu nehmen.“), wird das Item mit 1 bepunktet. Wird die Aussage gewählt, die nicht für Narzissmus steht (hier Aussage 2: „Ich kann nicht besonders gut Einfluss auf jemanden ausüben.“), wird eine 0 vergeben. Wie wir zu Beginn dieses Kapitels gelernt haben, können wir Item 1 wie folgt bepunkten:

Aber Vorsicht: so würden wir einen Fehler machen! Die Spalte npi$Q1 enthält nicht nur die Werte 1 und 2, sondern auch den Wert 0, wie wir mit dem Befehl table(npi$Q1) prüfen können:


   0    1    2 
  17 6872 4354 

Wie wir sehen, wurden die Antwortkategorien 0, 1 und 2 vergeben. Es kommt sogar 17 Mal die Antwortkategorie 0 vor – obwohl Antworten nur die Werte 1 und 2 annehmen dürfen. Wie kommt das? Die Antwort ist: Bei der Bearbeitung des NPI-Fragebogens – die im Rahmen einer Online-Studie stattfand – konnten Teilnehmer/innen Items unbeantwortet lassen. Fehlende Werte wurden mit einer 0 kodiert. Mit diesen fehlenden Werten müssen wir umgehen, bevor wir mit der psychometrischen Auswertung starten können.

Dafür wenden wir ein recht radikales Vorgehen an: Wir schließen einfach alle Personen aus, bei denen mindestens eine Antwort fehlt. Folgende Funktionen können uns dabei helfen:

  • complete.cases(): Gibt für einen data.frame aus, welche Zeilen keine fehlenden Werte enthalten (als logischen Vektor)
  • na.omit(): Gibt von einem data.frame nur die Zeilen ohne fehlende Werte aus

Der erste Schritt ist, alle fehlenden Werte – die derzeit mit 0 kodiert sind – in NA umzuwandeln. Nur NA wird von R tatsächlich als fehlend interpretiert; Konventionen, die Werte wie -99 oder 0 als fehlend klassifizieren, kennt R nicht.

Am liebsten würden wir im NPI-Datensatz alle Werte auf NA setzen, die per 0 als fehlend klassifiziert wurden. Dafür könnten wir ein Vorgehen verwenden, das wir in Kapitel 2 für Vektoren kennengelernt haben. Dieses Vorgehen funktioniert bei data.frames tatsächlich gleichermaßen:30

Dabei ergibt sich jedoch ein Problem: Fehlende Werte wurden im Datensatz zwar mit 0 kodiert, jedoch existiert eine Spalte (score), die den Wert 0 aus zwei Gründen enthalten kann:

  1. Der Wert fehlt.
  2. Eine Person hat in keinem der 40 Items der narzisstischen Aussage zugestimmt.

Der Gesamt-Testscore (npi$score) kann also wirklich den Wert 0 annehmen, wenn Teilnehmer/innen kein einziges Mal der narzisstischen Aussage zugestimmt haben – und dies kam tatsächlich 73 Mal vor. Würden wir alle Personen ausschließen, die in irgendeiner Spalte den Wert 0 haben, würden wir zu viele Personen ausschließen. Wir würden sogar eine Verzerrung in den Datensatz einfügen, da wir gezielt die Personen ausschließen, die besonders wenig narzisstisch sind. Hier war es also kein gutes Vorgehen, 0 als Kodierung für fehlende Werte zu verwenden. Stattdessen wird oftmals ein Wert verwendet, der selbst nicht Teil des legalen Wertebereichs ist – idealerweise sollte der Wert in gar keiner Spalte einer Datentabelle vorkommen können. Eine Konvention ist es, die Zahl -99 zu verwenden, die dieses Kriterium oftmals erfüllt.

Um Personen mit fehlenden Werten zu identifizieren, erstellen wir zunächst einen data.frame, der die Spalte score nicht enthält. Da wir die Spalte aber nicht gänzlich verlieren wollen, speichern wir den data.frame in einer neuen Variablen ab.

Nun setzen wir in diesem temporären data.frame alle Nullen auf NA:

Als Nächstes nutzen wir die Funktion complete.cases(), um herauszufinden, in welchen Zeilen von temp fehlende Werte vorliegen:

 [1]  TRUE  TRUE  TRUE  TRUE FALSE  TRUE  TRUE  TRUE  TRUE  TRUE

Die Funktion complete.cases() gibt einen logischen Vektor der Länge nrow(temp) zurück, der pro Zeile kodiert, ob irgendwo ein fehlender Wert vorliegt. Also können wir wie folgt überprüfen, wie viele Fälle mit fehlenden Werten es insgesamt gibt:

[1] 825

Ebenfalls können wir nun aus dem ursprünglichen Datensatz npi nur die vollständigen Fällen auswählen:

[1] 10418

Mit dem Datensatz npi_clean können wir nun psychometrische Berechnungen durchführen, da wir nur vollständige Datensätze ausgewählt haben. Dabei ist unser nächstes Ziel, für alle 40 Items eine dichotome Bepunktung durchzuführen. Wir wissen bereits, wie das funktioniert, nämlich indem wir mit ifelse() die Antworten auf jedes Item mit dem Schlüssel abgleichen. Der Schlüssel für das NPI kodiert für jedes der 40 Items den Wert, der für Narzissmus steht. Dies wäre wie folgt möglich:

Da wir nicht denselben Code – mit leichten Abwandlungen – 40 Mal wiederholen wollen, werden wir in Kapitel 7 lernen, diese Umkodierungen effizient durchzuführen, also auf einmal für alle 40 Spalten.

5.4 Fragen zum vertiefenden Verständnis

  1. Gegeben ist der Antwortvektor c(1, 2, 2, 1, 4, 5, 2, 2, 2, 3) und der Schlüssel 2. Wie kann ich die Item-Schwierigkeit ohne Anwendung der Funktion ifelse() bestimmen? Was ist die Item-Schwierigkeit?

  2. Gegeben ist der Antwortvektor c(2, 3, 2, 4, 5, 6, 2, 3), der die Antworten auf das Item eines Persönlichkeitsinventars enthält. Die Antworten wurden auf einer Likertskala gegeben, die zwischen 1 und 6 kodiert war. Da das Item negativ gepolt ist, müssen die Antworten vor der Analyse invertiert werden. Was ist der Mittelwert der umgepolten Antworten (d.h. die Item-Schwierigkeit)?


  1. Im Allgemeinen muss ein Schlüssel nicht Korrektheit anzeigen, sondern kann auch Merkmalsausprägung in einem Persönlichkeitsinventar kodieren. Wir werden das im Narcissistic Personality Inventory kennenlernen.

  2. http://www.burns-stat.com/documents/tutorials/why-use-the-r-language/

  3. Dieses wird gemeinsam mit den Daten des „Open Source Psychometrics Project“ https://openpsychometrics.org/_rawdata runtergeladen.

  4. Beachtet, dass hier ein Zugriff auf data.frames mit eckigen Klammern stattfindet (siehe Kapitel 3.5). Der Befehl sieht recht harmlos aus, aber tatsächlich steckt hier etwas mehr dahinter, als wir bislang behandelt haben. Wir nehmen zunächst einmal einfach hin, dass die Umkodierung von Werten in data.frames ebenso funktioniert wie bei Vektoren. Wie wir schon bei Vektoren kennengelernt haben, sind viele Operationen in R so allgemein, dass sie auf allen Daten eines Objekts stattfinden.