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:
- Umkodierung von Antworten
- Invertierung von Antworten
- 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?
- 14
- 16
- 19
- 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:
# Hypothetische Antworten auf das Bundesland Multiple-Choice-Item:
bundesland_answers <- c(1, 2, 1, 3, 2, 4, 2)
bundesland_key <- 2
bundesland_score <- ifelse(
test = bundesland_answers == bundesland_key,
yes = 1,
no = 0
)
bundesland_score
[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üsselyes
: Die Ausgabe an den Positionen, an denen dertest
TRUE
ergibtno
: Die Ausgabe an den Positionen, an denen dertest
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:
- Ich bin eher zurückhaltend, reserviert.
- 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:
item1i <- ifelse(big5$item1 == 1, 5, NA)
item1i <- ifelse(big5$item1 == 2, 4, item1i)
item1i <- ifelse(big5$item1 == 3, 3, item1i)
item1i <- ifelse(big5$item1 == 4, 2, item1i)
item1i <- ifelse(big5$item1 == 5, 1, item1i)
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:
- Wir wandeln alle Werte in
NA
um, die als fehlend zu klassifizieren sind - 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:
- Ich habe eine natürliche Begabung, auf Menschen Einfluss zu nehmen.
- 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 einendata.frame
aus, welche Zeilen keine fehlenden Werte enthalten (als logischen Vektor)na.omit()
: Gibt von einemdata.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:
- Der Wert fehlt.
- 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:
# Hier kein legaler R-Code, nur exemplarisch:
npi_key <- c(1, 1, ..., 2) # 40 Schlüsselemente
npi_clean$Q1_score <- ifelse(npi_clean$Q1 == npi_key[1], 1, 0)
# ...
npi_clean$Q40_score <- ifelse(npi_clean$Q40 == npi_key[40], 1, 0)
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
Gegeben ist der Antwortvektor
c(1, 2, 2, 1, 4, 5, 2, 2, 2, 3)
und der Schlüssel2
. Wie kann ich die Item-Schwierigkeit ohne Anwendung der Funktionifelse()
bestimmen? Was ist die Item-Schwierigkeit?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)?
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.↩
http://www.burns-stat.com/documents/tutorials/why-use-the-r-language/↩
Dieses wird gemeinsam mit den Daten des „Open Source Psychometrics Project“ https://openpsychometrics.org/_rawdata runtergeladen.↩
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 indata.frames
ebenso funktioniert wie bei Vektoren. Wie wir schon bei Vektoren kennengelernt haben, sind viele Operationen inR
so allgemein, dass sie auf allen Daten eines Objekts stattfinden.↩