-
Notifications
You must be signed in to change notification settings - Fork 0
/
085-aufbereiten.qmd
930 lines (571 loc) · 29 KB
/
085-aufbereiten.qmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
---
output: html_document
editor_options:
chunk_output_type: console
---
# Auswerten: Daten aufbereiten
```{r}
#| echo: false
library(ggplot2)
theme_set(theme_minimal())
library(nomnoml)
```
## Lernsteuerung
### Lernziele
- Sie können grundlegende Schritte der Datenaufbereitung in R durchführen.
- Sie können die typischen Schritte einer deskriptiven Datenanalyse durchführen in R.
- Sie können Grenzen Ihrer Auswertungsmethodik nennen.
### Position im Lernpfad
Sie befinden sich im Abschnitt "Auswertung" in @fig-ueberblick.
Behalten Sie Ihren Fortschritt im Projektplan im Blick, s. @fig-projektplan.
### Benötigte R-Pakete und Daten^[Haben ich eigentlich schon einmal erwähnt, dass Sie ggf. die Pakete zuerst (einmalig) installieren müssen, bevor Sie sie verwenden können?]
```{r}
#| messagen: false
library(tidyverse)
library(easystats)
```
```{r load-extra-web, eval = TRUE}
data_url <- "https://raw.githubusercontent.com/sebastiansauer/modar/master/datasets/extra.csv"
extra <- data_read(data_url) # aus `{easystats}`
```
Anstelle von `data_read` könnten Sie auch `read_csv` (Tidyverse) oder `read.csv` (Standard-R) verwenden.
## Typische Schritte beim Daten aufbereiten
Häufig sind Daten noch nicht "aufbereitet" und müssen noch "geputzt" oder "aufgeräumt" werden. Dazu gehören Schritte wie
- Datentabellen zusammenführen
- Daten umkodieren
- Daten aggregieren
- Daten gruppieren
- Fehlende Werte ersetzen
- Datenqualität prüfen
- Verteilungsformen prüfen
- Ausreißer behandeln
Betrachten wir im Folgenden einige zentrale Aspekte dieser Schritte.
### Datentabellen zusammenführen
Sie haben die Daten anhand mehrerer Fragebogen erhoben?
Oder Sie haben mehrere Gruppen untersucht?
Jetzt haben Sie *mehrere* Datentabellen, die
auf eine Analyse harren?
*Verheiraten* ist angesagt:
Sie müssen *alle* Teiltabellen in *eine* Tabelle (Datensatz) bringen.
Dazu kann man zwei Fälle unterscheiden:
1. Zeilenweises zusammenführen
2. Spaltenweises zusammenführen
Das kann man in Excel-Programmen erledigen,
aber auch in R.
Um es in R zu erledigen, müssen Sie erst eine Idee verstehen und dann etwas Syntax lernen. Warum sollten Sie die vielleicht 10 Minuten Lernaufwand investieren?
Ein Grund ist, dass es sinnvoll für Ihre berufliche Entwicklung ein Kernkonzept der Datenaufbereitung zu verstehen.
Ein anderer Grund ist, dass Sie mit einer Skriptsprache (wie R) ein "Kochrezept" erstellen, dass Sie ohne Zusatzaufwand wiederholt durchlaufen lassen können. Und, glauben Sie mir, in einer Datenanalyse passiert es oft, dass man mehrere (viele!) Durchläufe braucht, bis man mit dem Ergebnis zufrieden ist. Außerdem dokumentiert so ein Kochrezept Ihr Vorgehen transparent. Reproduzierbarkeit ist zentral für die Wissenschaft (und Technik).
#### Zeilenweises zusammenführen
:::{#exm-bind-rows}
*Jede Versuchsperson* Ihrer Studie hat zwei (oder mehrere) Fragebögen ausgefüllt. In Datensatz A finden sich die ersten paar Variablen und in Datensatz B die restlichen Variablen. Aber in beiden Datensätzen kommen alle Personen vor. $\square$
:::
@fig-bind-rows veranschaulicht das zeilenweise Zusammenführen.
![Tabellen zeilenweise zusammenführen](img/add-rowwise.svg){#fig-bind-rows width=50%}
Etwas ausführlicher ist der Sachverhalt im Listing @lst-bind-rows dargestellt.
Sagen wir, wir haben zwei Tabellen, die wir "untereinander" (also zeilenweise) "zusammenkleben" möchten.
Dann hilft uns `bind_rows()`.
:::{#lst-bind-rows}
```{r}
tab1 <- mtcars %>% slice(1:16)
tab2 <- mtcars %>% slice(17:32)
mtcars_gesamt <-
tab1 %>%
bind_rows(tab2) # aus `tidyverse`
```
:::
#### Spaltenweises Zusammenführen 1
:::{#exm-bind-cols}
*In jeder Gruppe* Ihrer Studie wurde ein anderer Fragebogen ausgefüllt (z.B. weil Sie unterschiedliche experimentelle Gruppen untersucht haben). Sie haben also zwei Datensätze mit unterschiedlichen Personen (nämlich in Datensatz A die Personen der Gruppe A und in Datensatz B die Personen der Gruppe B). Aber in jedem Datensatz finden sich die gleichen Variablen. $\square$
:::
@fig-bind-cols veranschaulicht das spaltenweise Zusammenführen von Tabellen.
![Tabellen spaltenweise zusammenführen](img/add-colwise.svg){#fig-bind-cols}
Sagen wir, wir haben zwei Tabellen, die wir "hintereinander kleben" möchten, also spaltenweise zusammenführen möchten, s. @lst-bind-cols.
:::{#lst-bind-cols}
```{r}
tab1 <- mtcars %>% select(1:5)
tab2 <- mtcars %>% select(6:11)
mtcars_gesamt <-
tab1 %>%
bind_cols(tab2)
```
:::
[Hier](https://dplyr.tidyverse.org/reference/bind.html) findet sich auch ein nützliches Beispiel.
:::{.callout-caution}
*Achtung* Diese Art des Zusammenfügens prüft *nicht*,
ob die Sortierung der Tabellen identisch ist:
Ob also die Zeile für "Schorsch" aus Tabelle 1 auch mit Schorschs Zeile aus Tabelle 2 zusammengefügt werden (und nicht etwas mit denen von Alois).
Sie müssen also selber dafür sorgen,
dass die Sortierung richtig ist.
Alternativ könnten Sie mit einem `join` arbeiten,
der die korrekte Sortierung für Sie übernimmt. $\square$
:::
#### Spaltenweises Zusammenführen 2
@fig-full-join veranschaulicht das spaltenweise Zusammenführen, wenn die Sortierung der Tabellen nicht übereinstimmt.
:::{#def-join}
#### Join zweier Tabellen
Das spaltenweise Zusammenführen zweier Tabellen unter der Beachtung der Sortierung nennt man einen "Join". Genauer gesagt versteht man unter "Sortierung" die Beachtung der Werte einer ID-Spalte (auch "Key-Spalte") genannt. $\square$
:::
![Das "Joinen" zweier Tabellen](img/full-join.gif){#fig-full-join width=50%}
Bildquelle: [Garrick Aden-Buie](https://www.garrickadenbuie.com/project/tidyexplain/), [CC-BY-SA 4.0](https://www.garrickadenbuie.com/colophon/)
### Text in Zahlen umwandeln
#### Hilfe, ich habe keine Zahlen
Kennen Sie das? Sie haben eine Umfrage durchgeführt, Daten sind erhoben, puh, bald können Sie das Projekt abschließen.
Jetzt haben Sie die Daten in R importiert,
aber müssen zu Ihrem Schrecken feststellen,
dass die Spalten (Variablen) die eigentlich Zahlen sein sollten,
als `character`, Text also, formatiert sind in R.
Anstelle der Zahl `5` steht in der Spalte also `"5"` (man beachte die Anführungszeichen, die anzeigen, dass es sich um einen Text handelt).
Na toll.
Mit Wörtern (Text) kann man nicht rechnen, und Sie rechnen doch so gern...
R weigert sich standhaft, mit Text zu rechnen:
```{r error = TRUE}
"5" + "5"
```
Hätten wir brave Zahlen, wäre alles paletti:
```{r}
5+5
```
Der Einfachheit halber erzeugen wir uns eine einfache Tabelle, mit ein paar Spalten,
die *als Text* formatierte Zahlen enthalten:
```{r}
d <- tibble(i01 = c("1", "3", "4"), # von 1 bis 4
i02 = c("-2", "+3", "-1"), # von -3 bis -3
i03 = factor(c("-2", "+2", "-1"))) # als Faktorvariable formatiert
d
```
Für diejenigen, die kompliziert mögen, ist hier noch eine `factor`-Spalte hinzugefügt.
Erstmal ignorieren.
Stellen Sie sich vor, die Tabelle ist ein Auszug aus Ihrer Umfrage,
wobei `i01` das erste Item (Frage) Ihres Fragebogens darstellt etc.
Wie kann man R beibringen,
dass die fraglichen Spalte `i01` doch "in Wirklichkeit" Zahlen sind und kein Text?
Welcher R-Befehl hilft hier?
#### Introducing `parse_number()`
`parse_number()` (aus `{tidyverse}`) löst das Problem für Sie:
```{r}
d2 <-
d %>%
mutate(i01_num = parse_number(i01))
d2
```
So würde es *in einigen Fällen* auch gehen:
```{r}
d %>% mutate(i01_r = as.numeric(i01))
```
Aber wenn `i01` als `factor()` formatiert ist, dann geht es nicht unbedingt.
```{r}
d %>% mutate(i02_r = as.numeric(factor(i02)))
```
Hoppla! Die Zahlen passen nicht!
`parse_number()` verlangt als Input `character`,
so dass Sie ggf. noch von `factor` auf `character` umformatieren müssen.
```{r}
d %>% mutate(i03_r = parse_number(as.character(i03)))
```
<!-- ## Daten umkodieren -->
<!-- In einer Fragebogenstudie (oder vergleichbarer Studie) liegen in der Regel pro Respondent (allgemeiner: pro Beobachtung) eine Reihe von Antworten auf Fragebogen-Items vor. -->
<!-- Manchmal liegen die Antworten noch nicht als Zahl vor, sondern als Text, etwa "stimme eher zu". -->
<!-- Diese Antwort könnte auf einer vierstufigen Skala einer 3 entsprechen. Eine einfache Möglichkeit zum Umkodieren eröffnet das Paket `sjmisc`. -->
<!-- Als Beispielaufgabe soll der Wert "Frau" in 1 umkodiert werden und "Mann" in 0; übrige Werte sollen in `NA` kodiert werden. -->
<!-- ```{r} -->
<!-- data_rec <- extra %>% -->
<!-- rec(sex, rec = "Frau = 1; Mann = 0; else = NA") -->
<!-- ``` -->
<!-- Dabei wird eine neue Variable (Spalte) erzeugt, deren Namen der alten Variable entspricht, plus dem Suffix `_r`, in diesem Fall also `sex_r`. Man beachte, dass Textwerte (Strings) *nicht* in Anführungsstriche gesetzt werden müssen, nur der ganze "Rekodierungsterm" muss einmal in Anführungsstriche gesetzt werden. -->
<!-- Prüfen wir das Ergebnis: -->
<!-- ```{r} -->
<!-- data_rec %>% -->
<!-- count(sex_r) -->
<!-- ``` -->
#### VERTIEFUNG Schleifen
:::{#def-schleife}
#### Schleife
Eine Schleife ist ein Operation, die einen Befehl (im Rahmen der computergestützten Datenanalyse) mehrfach (wiederholt) ausführt. Da im Kontext der Datenanalyse oft mit Tabellen gearbeitet wird, bezieht sich die Wiederholung oft auf eine Spalten-Operation, die dann für mehrere Spalten wiederholt wird. $\square$
:::
Wir können einen Befehl auf mehrere Spalten einer Tabelle anwenden,
und zwar mit `across` (aus `tidyverse`):
```{r}
d %>%
mutate(across(i01:i03, as.character))
```
Neben `as.character` könnte `parse_number` ein Befehl sein,
den man auf mehrere Spalten gleichzeitig anwenden will:
```{r}
d %>%
mutate(across(i01:i03, as.character)) %>%
mutate(across(i01:i03, parse_number))
```
Natürlich müssen Sie `across` nicht verwenden; Sie können auch "von Hand" die Spalten einzeln beatmen und jeweils (pro Spalte) `as.character` und `parse_number` anwenden.
### Items umkodieren
In Fragebogen werden immer wieder Items *negativ* kodiert. Das bedeutet, dass sie gegenteilig zum messenden Konstrukt formuliert sind. Ist das Konstrukt Extraversion, so würde ein negatives Item im Sinne von Introversion kodiert sein. Ein Beispiel-Item für negative Kodierung wäre: "Ich bin ein Couch-Potato" oder "Ich bleibe am liebsten alleine zuhause."
Zuerst müssen wir die Anzahl der Antwortstufen wissen; diese Information findet sich in der Dokumentation der Skala (im "Manual" auch "Testdokumentation" oder "Benutzerhandbuch" genannt). Natürlich kann man prüfen, welche Antwortstufen die Respondenten gefunden haben, aber man wäre nicht sicher, ob auch alle möglichen Antworten ausgeschöpft wurden.
Im vorliegenden Fall ist der Dokumentation des Instruments zu entnehmen, dass jedes Item vier Antwortstufen (Likertformat) aufweist. Likert-skalierte Items zeichnen sich dadurch aus, dass sie so formuliert sind, dass höhere Werte in der Antwortstufe mit höherer Ausprägung des zu messenden Konstrukts einher gehen.
Beim Umkodieren wird das Item "auf den Kopf gestellt": Der höchste Wert wird der kleinste, der zwei kleinste wird der zweitgrößte und so weiter. Im Schema sieht dies so aus:
```
alter Wert ist 1 --> neuer Wert ist 4
alter Wert ist 2 --> neuer Wert ist 3
alter Wert ist 3 --> neuer Wert ist 2
alter Wert ist 4 --> neuer Wert ist 1
alter Wert --> neuer Wert
```
#### Umkodieren mit `case_when`
Zum Umkodieren der Werte negativ kodierter Items bietet sich die Funktion `case_when` aus `tidyverse` an.
```{r}
extra |>
mutate(i01r = case_when(
i01 == 1 ~ 4,
i01 == 2 ~ 3,
i01 == 3 ~ 2,
i01 == 4 ~ 1
)) |>
select(i01, i01r) |>
head()
```
Die Syntax von `case_when()` lautet:
```
case_when(Prüfung ~ Konsequenz,
Prüfung2 ~ Konsequenz)
```
Nur wenn `Prüfung` erfüllt ist (den Wert `TRUE` hat), dann wird `Konsequenz` ausgeführt.
Ansonsten wird auf `Prüfung2` getestet, etc.
Vergessen Sie dann nicht, das Ergebnis (die resultierende Tabelle) als (neuen) Datensatz zu speichern.
#### Vertiefung: Umkodieren mit `recoce_values`
Zum Umkodieren der Werte negativ kodierter Items bietet sich die Funktion `recode_values` aus `easystats` an.
```{r eval = FALSE}
extra %>%
recode_values(select = i02r, # aus `easystats`
recode = list(`1`=4, `2`=3, `3`=2, `4`=1),
append = TRUE) %>%
select(i02, i02r) %>% # zeige nur die beiden Item i02r und i02r_r
head()
```
In diesem Fall ist das Item `i02r` bereits umkodiert -- genau wie alle Items im Datensatz die mit dem Suffix `r` gekennzeichnet sind.
Daher war das Umkodieren hier nicht nötig -- sogar *falsch*, um genau zu sein.
Das diente nur zu Übungszwecken.
In anderen Situationen kann es aber nötig sein, Items umzukodieren.
#### Vertiefung: Umkodieren mit Rechenregel
Alternativ können Sie auch folgende Rechenmethode zum Umkodieren von Items verwenden:
$$\text{neuer Wert} = \text{Anzahl Kategorien} + 1 - \text{alter Wert}$$
Diese Rechenmethode funktioniert aber nur, wenn z.B. bei vier Antwortstufen die Ausprägungen mit 1,2,3,4 kodiert sind. Allgemeiner: Bei $n$ Ausprägungen müssen die Werte mit $1,2, \ldots, n$ kodiert sein, so dass die Ausprägungen den Werten entsprechen.
:::{#exm-umkodieren}
Wir kodieren Item 1 (`i01`) um:
```{r}
extra_umcodiert_i01 <-
extra |>
mutate(i01r = 5 - i01)
extra_umcodiert_i01 |>
select(i01, i01r) |>
head()
```
:::
### Item-Labels in Zahlen umwandeln
Eine ähnliche Sache wie das Umpolen von Items ist folgende Situation: Ihre Umfrage-Software spuckt als Item-Antworten die "Ankertexte" oder "Itemlabels" aus wie *stimme überhaupt nicht zu* oder *stimme voll und ganz zu*.
Da man bekanntlich (höchstens) mit Zahlen aber nicht mit Texten rechnen kann,
möchten Sie diese Itemlabels in Zahlen umwandeln.
Das sieht dann ähnlich zu folgendem Wenn-Dann-Regel-Schema aus:
```
stimme voll und ganz zu --> 4
stimme eher zu --> 3
stimme eher nicht zu--> 2
stimme überhaupt nicht zu --> 1
```
Dieses Umwandeln können Sie mit `case_when()` vornehmen.
`case_when()` listet einfach Wenn-Dann-Regeln auf.
Erzeugen wir uns mal eine einfache Tabelle,
um die Sache zu illustrieren:
```{r}
d <-
tibble(person_nr = 1:3,
i01 = c("stimme überhaupt nicht zu", "stimmer eher zu", "stimme voll und ganz zu"))
d
```
So ähnlich würden also Ihre Daten aussehen.
Jetzt zum Umwandeln:
```{r}
d %>%
mutate(i01_r = case_when(
i01 == "stimme überhaupt nicht zu" ~ 1,
i01 == "stimme eher nicht zu" ~ 2,
i01 == "stimme eher zu" ~3,
i01 == "stimme voll und ganz zu" ~ 4
))
```
Eine alternativer Weg des Umkodierens ist `recode_value()` aus `easystats` zu nutzen.
### Fehlende Werte ersetzen
Wie geht man mit fehlenden Werten um? Das ist im Detail keine leichte Frage. Eine einfache Antwort ist: alle Zeilen mit fehlenden Werten löschen. Das hat aber den Nachteil, dass man ggf. viele Daten verliert. Eine andere einfache Antwort lautet: Nichts machen. Das kann ein sinvoller Start sein; zumindest macht man nichts kaputt.
Was die ID-Spalte betrifft, sollte man aber fehlende Werte ersetzen, damit man jede Zeile identifizieren kann.
```{r replace-na-in-id-col}
extra_nona_in_id_col <-
extra |>
mutate(code = replace_na(code, "fehlt")) # aus tidyverse
```
:::{#exr-nona-id}
Betrachten Sie beide Tabellen (mit der Tabellenansicht in RStudio) und finden Sie den Unterschied in der Spalte `code`. $\square$
:::
### Zeilen (Fälle) löschen
#### Zeilen löschen nach ID-Spalte
In jeder braven Tabelle steht pro Zeile (genau) ein Fall;
mit "Fall", auch als "Beobachtung" bezeichnet, ist ein Untersuchungsobjekt gemeint.
Häufig sind das in der Psychologie Personen, es könnten aber auch z.B. Versuchsdurchgänge, Teams oder Firmen sein.
Sagen wir, Sie möchten einen bestimmten Fall löschen.
Der Einfachheit halber nehmen wir den ersten Fall der Tabelle `extra`.
Hier hat jeder Fall schon eine "ID", eine unique, also eineindeutige Zuordnung,
das ist die Spalte `code`. Auch die Spalte `timestamp` taugt vermutlich für eine Zuordnung.
In R könnten Sie die erste Zeile also anhand dieser ID-Spalte ansprechen und entsprechend filtern. Sagen wir, wir möchten die Person/Datenzeile mit der ID "HSC" entfernen:
```{r}
extra_kurz <-
extra_nona_in_id_col %>%
filter(!(code == "HSC")) # ! ist die logische Verneinung
```
#### Vertiefung
Schauen Sie sich mal diese Variante an:
```{r}
extra_kurz <-
extra %>%
filter(code != "HSC" | is.na(code)) # ! ist die logische Verneinung
```
Falls die ID-Spalte (`code`) fehlende Werte hat, könnten Sie sich wundern: `extra |> filter(code != "HSC")` hätte zwar die Zeile, in der bei `code` der Wert `HSC` steht entfernt, aber *auch* *alle* Zeilen, in denen bei `code` `NA` steht.
Darum haben wir die fehlenden Werte explizit drinnen gelassen.
Die technischen Details für dieses vorsichtige Verhalten von `filter()` finden sich [hier](https://stackoverflow.com/questions/28857653/removing-na-observations-with-dplyrfilter).
Prüfen wir die Anzahl der Zeilen:
```{r}
nrow(extra_kurz)
nrow(extra)
```
Falls Ihr Datensatz über keine ID-Spalte verfügt, können Sie so eine hinzufügen:
```{r}
extra2 <-
extra %>%
mutate(id = 1:nrow(extra)) %>% # nrow liefert die Anzahl der Zeilen der Tabelle zurück
relocate(id, .before = 1) # wir ziehen die ID-Variable an die erste Spalte
```
Und jetzt können wir entspannt filtern:
```{r}
extra_kurz <-
extra2 %>%
filter(id != 1)
```
<!-- ### Vertiefung: Duplikate entfernen -->
### Datenqualität prüfen
Bevor wir die Daten interpretieren, müssen wir sie auf Herz und Nieren prüfen,
ein Daten-Gesundheitscheck^[Sanity Check, hört sich cooler an].
Ein paar typische Probleme, die man immer wieder findet, und die wir gelöst haben wollen, sind:
- *Dubletten* (doppelte Zeilen/Fälle) in den Daten
- Eingabefehler (unplausible oder unmögliche Werte)
- Zu schnelle Bearbeitungszeit (pro Item 1 Sekunde Bearbeitungszeit erlaubt nicht wirklich überlegte Antworten)
- *Geringe Eingabequalität*: Versuchspersonen haben "Blümchen gekreuzt", also keine ernsthaften Antworten gegeben
- ...
#### Auf Dublikate prüfen
Betrachten wir der Einfachheit halber folgenden Datensatz, in dem die erste und zweite Zeile identisch sind; die zweite Zeile ist also eine Dublette (Dublikat).
Die dritte Zeile ist unique.
```{r}
df <-
tibble(id = c(1, 2, 3),
x = c("a", "a", "yeah"),
y = c(5, 5, 3000))
df
```
Genau genommen hat `df` keine Dublikate, aber wenn wir die Spalte `id` ausklammern, dann schon:
```{r}
df |>
select(2, 3)
```
Dann kann man `distinct()`, nutzen, um Dublikate zu entfernen:
```{r}
df |>
select(2, 3) |>
distinct()
```
Man kann `distinct()` auch nutzen, um nur in bestimmten Spalten auf Dublikate zu suchen:
```{r}
df |>
distinct(x, y, .keep_all = TRUE)
```
Ohne das `.keep_all = TRUE` werden nur die Spalten `x` und `y` zurückgeliefert (also die Spalten, die wir bei `distinct()` ausgewählt hatten):
```{r}
df |>
distinct(x, y)
```
#### Auf Eingabefehler prüfen
Sagen wir, für Variable `x` ist nur ein einzelner Buchstabe erlaubt;
alles andere ist ein Eingabe- oder sonstiger Fehler.
Eine Variable vom Typ "Text" heißt in R `Character` oder `String`.
Mit `str_length(x)` bekommt man entsprechend die Länge eines Strings.
Hier prüfen wir, ob die String-Länge 1 ist, dann alles okay.
Wenn die String-Länge von `x` (für eine bestimmte Zeile) ungleich 1 ist,
dann nicht-okay:
```{r}
df_check <-
df %>%
mutate(x_check_okay = case_when(
str_length(x) == 1 ~ TRUE,
str_length(x) != 1 ~ FALSE
))
df_check
```
Jetzt könnten wir alle "bösen" Zeilen rausschmeißen:
```{r}
df2 <-
df_check %>%
filter(x_check_okay)
df2
```
Als Nächstes prüfen wir, ob `y`, eine metrische (numerische) Variable den richtigen Wertebereich hat.
Sagen wir, Es sind nur positive Werte nicht größer als 5 erlaubt, also `0<x<=5`.
```{r}
df %>%
mutate(y_check_okay = case_when(
0<y & y<=5 ~ TRUE, # wenn y im erlaubten Wertebereich, dann okay
TRUE ~ FALSE # ansonsten: nicht-okay
))
```
`case_when` arbeitet die Prüfbedingungen *zeilenweise* ab.
Ist die erste Zeile (`0<y & y<=5`) nicht erfüllt, dann können wir R sagen: "Immer nicht okay",
das erreichen wir mit `TRUE` (`TRUE` ist immer wahr) `~ FALSE` (`y_check_okay` wird auf `FALSE`) gesetzt.
Im Anschluss könnten wir wieder die "bösen Zeilen" herausfiltern.
#### Geringe Eingabequalität
Vielleicht hätte ihr Fragebogen nicht so lang sein sollen, 60 Minuten Befragung war dann wohl doch etwas viel.
Erfahrungsgemäß verliert man pro Seite Fragebogen substanziell Versuchspersonen.
Zehn Minuten sind für viele Menschen schon eine lange Befragung.
Also: Beim nächsten Mal kürzer halten.
Schlimmer noch als keine Antworten sind allerdings schlechte Antworten,
wenn Versuchspersonen (aus Demotivation heraus) "Blümchen" kreuzen.
Häufig bedeutet das, dass einfach immer die erste Antwortoption angekreuzt wird;
das ist am einfachsten für die Versuchsperson.
Wir stellen uns dabei Fragen wie:
- Hat eine Versuchsperson wenig (keine) Varianz in ihrem Antwortverhalten?
- Hat eine Versuchsperson viel mehr Varianz als die (allermeisten) anderen?
Technisch gesprochen prüfen wir pro Person mehrere Spalten eines Datensatzes,
etwa indem wir die Varianz berechnen.
Eine Komplikation ist, dass Datensätze in R *spaltenweise* aufgebaut sind,
wir aber das Ergebnis *pro Zeile* (Versuchsperson) haben möchten.
```{r}
extra2 <-
extra %>%
rowwise() %>%
mutate(extra_var = var(c_across(i01:i10)))
extra2 %>%
select(extra_var) %>%
arrange(extra_var) %>%
head()
```
:::{.callout-note}
`c_across()` hat das gleiche Ziel wie `c()`.
Da aber `c()` bei zeilenweisen Operationen nicht verwendet kann (das checkt `c()` leider nicht), greifen wir auf seinen Cousin, `c_across()` zurück, der das kann. $\square$
:::
Aber was ist viel und was ist weniger Varianz in diesem Zusammenhang?
Am besten schauen wir uns mal die typische Varianz der Itemantworten für diesen Datensatz an.
```{r}
library(ggpubr)
extra2 %>%
gghistogram(x = "extra_var")
```
Auf dieser Basis könnte man Fälle (Versuchspersonen, d.h. Zeilen) entfernen,
die keine Varianz oder eine Varianz höher als 1.75 aufweisen.
```{r}
extra3 <-
extra2 %>%
filter(extra_var > 0 & extra_var < 1.76)
```
### Scores berechnen
#### Summen- und Mittelwerte
In der Psychometrie werden komplexe Konstrukte wie etwa das Persönlichkeitsmerkmal Extraversion anhand mehrerer Indikatoren (meistens Items eines Fragebogens) gemessen.
Um zu einem Personenwert für Extraversion zu gelangen, werden die Itemwerte im einfachsten Fall summiert. Alternativ kann man auch einen Mittelwert bilden.
Dieses Aggregieren bietet den Vorteil, dass sich Messfehler (möglicherweise) herausmitteln.
Außerdem versucht man so abzubilden, dass Extraversion aus mehreren unterschiedlichen Facetten besteht, die nicht mit einem einzelnen Item, sondern über mehrere unterschiedliche Items,
erfasst werden. Viele Psychometriker sind skeptisch, wenn man versuchen würde,
Extraversion mit der Frage "Wie extrovertiert sind Sie?" zu erfassen.
Ihre Bedenken sind, dass Menschen die vielen Facetten von Extraversion nicht im Arbeitsgedächtnis vorhalten können.
Fragt man hingegen nur einen kleinen Aspekt von Extraversion ab, trägt man der Breite des Konstrukts nicht Rechnung.
Die Zusammenfassung der Itemwerte eines Konstrukts zu einem Mittelwert oder einem Summenwert bezeichnet man als *Score*.
Ein einfaches Beispiel zur Berechnung des Extraversion-Summenscore:
```{r extra-bsp1, echo = TRUE}
extra_bsp <- extra %>%
select(i01:i03) %>%
slice_head(n = 3) %>%
mutate(extra_sum = i01 + i02r + i03)
extra_bsp
```
Der Wert von `extra_sum` berechnet sich jeweils als Summe der drei Itemwerte.
Mit dem Mittelwert verhält es sich analog (s. @tbl-extra-score-mean).
```{r extra-bsp2}
extra_bsp <- extra_bsp %>%
mutate(extra_mean = extra_sum / 3)
```
```{r}
#| tbl-cap: Extraversion-Score berechen
#| echo: false
#| label: tbl-extra-score-mean
extra_bsp %>%
print_md()
```
Praktischerweise gibt es Funktionen, die die Berechnung eines Scores noch weiter vereinfachen,
zum Beispiel im Paket `easystats`: `row_means()` (Mittelwert pro Person).
Da Respondenten (meist Personen) in *Zeilen* stehen heißen die Befehle `row_XXX()`.
Fragt sich noch, ob es mehr Sinn macht, einen Summenscore oder einen Mittelwert zu berechnen.
Kurz gesagt macht es keinen großen Unterschied, solange es keine fehlenden Werte gibt.
Gibt es aber fehlende Werte, sollte man Mittelwerte statt Summenwerte vorziehen.
#### Vertiefung: Summen- vs. Mittelwertscores
Dazu ein erläuterndes Beispiel.
Alois habe in einem Persönlichkeitstest mit 3 Items nur Item 1 beantwortet und zwar mit "3", wobei Antwortstufen von 1 bis 4 vorgegeben waren. Vermutlich ist der Gesamtwert im Form des Summenscores von 3 zu klein, unterschätzt als Alois' Wert.
Schließlich hat er beim ersten Item die Antwort "3" gewählt, insofern ist es plausibel, dass er bei den anderen auch diese Option gewählt hätte.
Somit hätte er insgesamt 9 Punkte (nicht 3) erzielt. Würden wir 3 als Gesamtwert (Summenscore) annehmen, so bedeutet das, das wir davon ausgehen, dass er im Schnitt "1" gewählt hat -
Eine Annahme, die nicht sehr plausibel erscheint.
Vergleichen wir das mit dem Mittelwert-Score. Jetzt lassen wir R die Rechenarbeit machen:
```{r}
Alois <- c(3, NA, NA)
mean(Alois, na.rm = TRUE)
```
Der Mittelwert von Alois beträgt 3 -- das passt genau zu unserer Argumentation von gerade (s. oben), dass 3 eine bessere Schätzung der Ausprägung der latenten Variable von Alois ist.
Daher ist der Mittelwert dem Summenscore vorzuziehen.
Ein anderer Vorteil des Mittelwerts ist, dass er etwas anschaulicher ist als der Summenscore: Ein Mittelwert von 3 (auf einer Skala von 1 bis 4) ist anschaulicher als eine Summe von 9 (bei drei Items). Wir werden daher den Mittelwert vorziehen.
Noch anschaulicher wäre es, die Mittelwerte auf den Bereich von 0 bis 1 zu normalisieren.
#### Berechnung mit R
Wie in jeder Sprache gibt es auch in R oft mehrere Möglichkeiten, um eine Sache auszudrücken.
Betrachten wir im Folgenden, wie man einen *Score*, also einen *Zeilenmittelwert*, berechnet.
##### Mit `easystats`
`row_means` wohnt im Paket `easystats`.
```{r}
extra %>%
mutate(extra_avg =
row_means(extra, select = i01:i10, min_valid = .9)) |>
select(extra_avg) %>%
slice_head(n = 3)
```
Der Parameter `min_valid` bei `row_means()` gibt den Anteil der *nicht* fehlenden Werte (pro Zeile) wieder, damit ein Wert berechnet wird: Bei zu vielen fehlenden Werten (zu wenig Daten) pro Person wird sonst `NA` zurückgeliefert. Das ist sinnvoll, denn hat eine Person von 10 Items nur 1 Item beantwortet, so kann man wohl nicht zuverlässig sagen, dass Extraversion in seiner Breite zuverlässig geschätzt wird. Die Funktion fügt dem Datensatz eine Spalte hinzu, deren Name mit `var` angegeben wird.
##### Mit `tidyverse`
Die Idee von `rowwise()` ist es, den Datensatz so in Gruppen einzuteilen, dass jede Zeile eine Gruppe ist. Dann wird für jede "Gruppe" (Zeile) eine normale Operation durchgeführt.
Leider versteht der `c()`-Befehl es nicht, wenn man ihm mehrere Spalten (Vektoren) als Input gibt. `i01` und `i02` etc. sind technisch gesehen jeweils Vektoren. Der `c()`-Befehl verkraftet leider nur *einen* Vektor mit Daten als Input. Daher brauchen wir einen verwandten Befehl, der wie `c()` funktioniert, aber mehrere Vektoren als Eingabe akzeptiert: Enter `c_across()`.
```{r}
extra |>
rowwise() |>
mutate(extra_avg = mean(c_across(i01:i10))) |>
select(extra_avg) %>%
ungroup() |>
slice_head(n = 3)
```
### Standardisieren mit z-Werten
Man kann die Aussagekraft eines Mittelwerts noch erhöhen, in dem man ihn standardisiert, z.B. mit der z-Transformation. Das geht zum Beispiel so:
```{r}
extra_std <- extra %>%
standardize(select = extra_mean, append = TRUE)
```
Die Funktion `standardize()` z-standardisiert eine oder mehrere angegebene Spalten. Setzt man den Schatler `append` auf `TRUE`, werden neue Spalten erzeugt, deren Namen gleich dem alten Namen plus dem Suffix `_z` entspricht. Betrachten wir die ersten drei Zeilen:
```{r}
extra_std %>%
select(extra_mean) %>%
slice_head(n = 3)
```
Zu beachten ist, dass der Mittelwert der Stichprobe und deren Standardabweichung als Referenzwerte herangezogen wurden, nicht die entsprechenden Größen der Normierungsstichprobe.
## Aufgaben
Im Folgenden ist eine Auswahl an Aufgaben zur praktischen Datenanalyse vorgestellt. Das sind genau die Probleme, auf die Sie treffen (könnten), wenn Sie Ihre eigenen Daten auswerten, jetzt oder bei einer späteren Analyse.
Hier ist eine Auwahl an Aufgaben auf dem Bereich "Datenjudo":
- [tidy1](https://datenwerk.netlify.app/posts/tidy1/tidy1)
- [ausreisser1](https://datenwerk.netlify.app/posts/ausreisser1/ausreisser1)
- [na-per-col](https://datenwerk.netlify.app/posts/na-per-col/na-per-col)
- [filter-na1](https://datenwerk.netlify.app/posts/filter-na1/filter-na1)
- [filter-na2](https://datenwerk.netlify.app/posts/filter-na2/filter-na2)
- [filter-na4](https://datenwerk.netlify.app/posts/filter-na4/filter-na4)
## Fallstudien
- [Seitensprünge](https://data-se.netlify.app/2021/05/28/yacsda-seitenspr%C3%BCnge/)
- [London Marathon](https://lsinks.github.io/posts/2023-04-25-tidytuesday-marathon/marathon.html)