Table of Content
- 1 Introduction
- 2 purrr::map() function
- 3 How to request with map() some statistical values
- 4 How to use map() for multiple t-tests
- 5 How to use map() for multiple linear regressions
- 6 Conclusion
library(tidyverse)
library(purrr)
library(broom)
library(knitr)
library(psych)
affairs <- read_csv("Affairs.csv")
1 Introduction
Sehr häufig werden R-Befehle per Copy & Paste vervielfacht und anschließend für unterschiedliche Zwecke leicht abgewandelt. Dies ist nicht nur umständlich, sondern auch leicht fehleranfällig. Vor diesem Hintergrund werden im Nachfolgenden Möglichkeiten aufgezeigt, wie man effektive Codes in R schreiben kann, ohne einen Befehl doppelt zu verwenden.
Für diesen Post wurde der Datensatz Affairs von der Statistik Plattform “Kaggle” verwendet. Eine Kopie des Datensatzes ist unter https://drive.google.com/open?id=1N4osROEo724c7OZA2ARiwEthcZDwLxtf abrufbar.
2 purrr::map() function
Als kurze Einführung wird die Frage beantwortet, was die map Funktion aus dem purrr Package macht.
v <- list(1, 2, 3)
map(v, ~ .* 10)
## [[1]]
## [1] 10
##
## [[2]]
## [1] 20
##
## [[3]]
## [1] 30
Die map Funktion transformiert einen Input durch die Anwendung einer Funktion auf jedes Element des Inputs und gibt anschließend einen Vektor aus, der dieselbe Länge besitzt wie die des Inputs. In unserem Beispiel wurde R der Befehl gegeben, jedes Element aus v (zuvor erstelltes Objekt) mit 10 zu multiplizieren.
3 How to request with map() some statistical values
glimpse(affairs)
## Observations: 601
## Variables: 10
## $ X1 <int> 4, 5, 11, 16, 23, 29, 44, 45, 47, 49, 50, 55, 64...
## $ affairs <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...
## $ gender <chr> "male", "female", "female", "male", "male", "fem...
## $ age <dbl> 37, 27, 32, 57, 22, 32, 22, 57, 32, 22, 37, 27, ...
## $ yearsmarried <dbl> 10.00, 4.00, 15.00, 15.00, 0.75, 1.50, 0.75, 15....
## $ children <chr> "no", "no", "yes", "yes", "no", "no", "no", "yes...
## $ religiousness <int> 3, 4, 1, 5, 2, 2, 2, 2, 4, 4, 2, 4, 5, 2, 4, 1, ...
## $ education <int> 18, 14, 12, 18, 17, 17, 12, 14, 16, 14, 20, 18, ...
## $ occupation <int> 7, 6, 1, 6, 6, 5, 1, 4, 1, 4, 7, 6, 6, 5, 5, 5, ...
## $ rating <int> 4, 4, 4, 5, 3, 5, 3, 4, 2, 5, 2, 4, 4, 4, 4, 5, ...
Für die Beschreibung eines Datensatzes werden häufig sehr viele statistische Kennzahlen (Median, Mittelwert, Standardabweichung, …) angefordert. Dies jeweils mit einem eigenen Befehl zu tun ist besonders bei großen Datensätzen mit vielen Variablen sehr zeitintensiv und auch völlig überflüssig, denn hierfür kann prima auch die map Funktion verwendet werden.
Zu Beginn einer Analyse ist meist von Interesse, ob und wenn ja wie viele fehlende Werte im Datensatz vorhanden sind. Dies kann folgendermaßen überprüft werden.
affairs %>%
map(~sum(is.na(.)))
## $X1
## [1] 0
##
## $affairs
## [1] 0
##
## $gender
## [1] 0
##
## $age
## [1] 0
##
## $yearsmarried
## [1] 0
##
## $children
## [1] 0
##
## $religiousness
## [1] 0
##
## $education
## [1] 0
##
## $occupation
## [1] 0
##
## $rating
## [1] 0
Man sieht, dass bei keiner Variablen des Datensatzes “affairs” ein fehlender Wert (NA) existiert. Dieselbe Überprüfung kann auch pro Zeile erfolgen. Aus Übersichtsgründen wurde die Ausgabe des Befehls hier unterdrückt.
apply(affairs,MARGIN = 1, FUN = function(x) sum(is.na(x)))
Mit dem select Befehl können auch gezielt Spalten ausgewählt werden, für die anschließend mit der map Funktion die Kennzahlen angefordert werden sollen.
affairs %>% select(7:10) %>% map(describeBy)
## $religiousness
## vars n mean sd median trimmed mad min max range skew kurtosis
## X1 1 601 3.12 1.17 3 3.12 1.48 1 5 4 -0.09 -1.02
## se
## X1 0.05
##
## $education
## vars n mean sd median trimmed mad min max range skew kurtosis se
## X1 1 601 16.17 2.4 16 16.21 2.97 9 20 11 -0.25 -0.32 0.1
##
## $occupation
## vars n mean sd median trimmed mad min max range skew kurtosis
## X1 1 601 4.19 1.82 5 4.34 1.48 1 7 6 -0.74 -0.79
## se
## X1 0.07
##
## $rating
## vars n mean sd median trimmed mad min max range skew kurtosis se
## X1 1 601 3.93 1.1 4 4.07 1.48 1 5 4 -0.83 -0.22 0.04
Meistens sind jedoch nur gezielte Kennzahlen von bestimmten Variablen von Interesse. Der Datensatz Affairs wird deshalb auf folgende 5 Variablen beschränkt.
affairs.short <- affairs %>% select(age, 7:10)
affairs.short
## # A tibble: 601 x 5
## age religiousness education occupation rating
## <dbl> <int> <int> <int> <int>
## 1 37 3 18 7 4
## 2 27 4 14 6 4
## 3 32 1 12 1 4
## 4 57 5 18 6 5
## 5 22 2 17 6 3
## 6 32 2 17 5 5
## 7 22 2 12 1 3
## 8 57 2 14 4 4
## 9 32 4 16 1 2
## 10 22 4 14 4 5
## # ... with 591 more rows
Es soll nun der Mittelwert für alle 5 Variablen angefordert werden. Dafür würde sich die for loop Funktion anbieten.
output <- vector("double", length(affairs.short))
for (i in seq_along(affairs.short)) {
output[[i]] <- mean(affairs.short[[i]])
}
output
## [1] 32.487521 3.116473 16.166389 4.194676 3.931780
Man könnte dies auch gleich als Funktion schreiben.
col_mean <- function(df) {
output <- vector("double", length(df))
for (i in seq_along(df)) {
output[i] <- mean(df[[i]])
}
output
}
col_mean(affairs.short)
## [1] 32.487521 3.116473 16.166389 4.194676 3.931780
Man sieht, dass die Funktion klappt. Leicht abgewandelt, kann diese auch andere statistische Kennwerte berechnen. Im Nachfolgenden werden die Befehle auf die gleiche Art und Weise für die Standardabweichung und den Median erstellt.
col_sd <- function(df) {
output <- vector("double", length(df))
for (i in seq_along(df)) {
output[i] <- sd(df[[i]])
}
output
}
col_median <- function(df) {
output <- vector("double", length(df))
for (i in seq_along(df)) {
output[i] <- median(df[[i]])
}
output
}
Anschließend erfolgt eine gemeinsame Ausgabe der statistischen Werte.
col_mean(affairs.short)
## [1] 32.487521 3.116473 16.166389 4.194676 3.931780
col_sd(affairs.short)
## [1] 9.288762 1.167509 2.402555 1.819443 1.103179
col_median(affairs.short)
## [1] 32 3 16 5 4
Auch dies funktioniert wie erwartet. Für eine bessere Übersicht, können die Zeilen miteinander verbunden werden.
Mittelwert<-col_mean(affairs.short)
Median<-col_median(affairs.short)
standardabweichung<-col_sd(affairs.short)
cbind(Mittelwert,Median ,standardabweichung)
## Mittelwert Median standardabweichung
## [1,] 32.487521 32 9.288762
## [2,] 3.116473 3 1.167509
## [3,] 16.166389 16 2.402555
## [4,] 4.194676 5 1.819443
## [5,] 3.931780 4 1.103179
An und für sich ein zufriedenstellendes Ergebnis. Jedoch wurde wieder ein Fehler gemacht, der in diesem Post eigentlich vermieden werden sollte. Es wurde erneut copy & paste angewendet.
Die zentrale Frage ist: wie können die drei Funktionen in einer Syntax miteinander verbunden werden?
Zur Beantwortug dieser Frage werden beispielhaft folgende drei Funktionen erstellt.
fun1 <- function(x) abs(x - mean(x))^1
fun2 <- function(x) abs(x - mean(x))^2
fun3 <- function(x) abs(x - mean(x))^3
Diese in einer Syntax zu schreiben könnte folgendermaßen aussehen:
fun.ges <- function(x, i) abs(x - mean(x))^i
Genau dieses Prinzip lässt sich auch auf die Funktionen der statistischen Kennwerte anwenden.
col_summary <- function(df, fun) {
out <- vector("double", length(df))
for (i in seq_along(df)) {
out[[i]] <- fun(df[[i]])
}
out
}
Die erstellte col_summary Funktion lässt sich nun beliebig auf jeden Datensatz (Argument 1) und R-Befehl (Argument 2) anwenden. Hier die Anforderung des Medians für die Variablen im Datensatz “affairs.short”
col_summary(affairs.short, median)
## [1] 32 3 16 5 4
Man sieht das gleiche Ergebnis wie bei der zuvor erstellten “Einzelfunkton” für den Median (col_median(affairs.short)).
4 How to use map() for multiple t-tests
Mit map lässt sich ein kompletter Datensatz auch sehr schnell und effizient auf beispielsweise Geschlechterunterschiede mittels t-tests untersuchen.
affairs %>% select(age, affairs, yearsmarried, education, religiousness) %>%
map(function(x) t.test(x ~ affairs$gender)$p.value)
## $age
## [1] 2.848452e-06
##
## $affairs
## [1] 0.7739606
##
## $yearsmarried
## [1] 0.458246
##
## $education
## [1] 9.772643e-24
##
## $religiousness
## [1] 0.8513998
Männer und Frauen unterscheiden sich also signifikant in dieser Stichprobe nur in ihrem Alter und der Ausbildung.
affairs %>% select(age, affairs, yearsmarried, education, religiousness) %>% map(function(x) t.test(x ~ affairs$gender)$p.value) %>%
as.data.frame %>% gather %>% mutate(signif = ifelse(value < .05, "significant", "n.significant")) %>%
ggplot(aes(x = reorder(key, value), y = value)) +
geom_point(aes(color=signif)) +
coord_flip() +
ylab("p-value")
5 How to use map() for multiple linear regressions
Was mit dem t-Test ging, geht auch für die einfache lineare Regression. Hierbei wird aber nicht der p-Wert angefordert, sondern R2.
affairs %>%
select(age, yearsmarried, education, religiousness, rating, children, gender, occupation) %>%
map(~lm(affairs ~ .x, data = affairs)) %>%
map(summary) %>%
map_dbl("r.squared") %>%
tidy %>%
arrange(desc(x)) %>%
rename(r.squared = x) -> modell
## Warning: 'tidy.numeric' ist veraltet.
## Siehe help("Deprecated")
kable(modell)
names | r.squared |
---|---|
rating | 0.0781272 |
yearsmarried | 0.0349098 |
religiousness | 0.0208806 |
children | 0.0108181 |
age | 0.0090701 |
occupation | 0.0024613 |
gender | 0.0001377 |
education | 0.0000059 |
Der map Befehl sieht hier anders aus, als der beim t-Test verwendete, ist aber nur eine andere Schreibweise. Hier die beiden Varianten gegenüber gesellt:
- map(~lm(affairs ~ .x, data = affairs))
- map(function(x) lm(affairs$affairs ~ .x))
Zur Überprüfung wird eine Regression manuell durchgeführt.
lm1 <- lm(affairs ~ rating, data = affairs)
summary(lm1)$r.squared
## [1] 0.07812718
Wir sehen das gleiche R2 wie bei der Ausgabe kable(modell).
Wie viele Regressionen wurden eigentlich durchgeführt?
ncol(affairs %>%
select(age, yearsmarried, education, religiousness, rating, children, gender, occupation))
## [1] 8
Insgesamt 8 Stück. Es hat sich also bereits gelohnt die map Funktion anzuwenden. Die Ergebnisse sollen nun noch schön graphisch dargestellt werden.
ggplot(modell, aes(x = reorder(names, r.squared), y = r.squared)) +
geom_point(size = 5, color = "green") +
ylab(expression(R^{2})) +
xlab("Prädiktoren") +
ggtitle("Erklärte Varianz pro Prädiktor")
Der Prädiktor rating
erklärt also mit knapp 8% am meisten Varianz.
Im zweiten Schritt soll noch überprüft werden, ob die Ergebnisse signifikant sind.
affairs %>%
select(age, yearsmarried, education, religiousness, rating, children, gender, occupation) %>%
map(~lm(affairs ~ .x, data = affairs)) %>%
map(summary) %>%
map(c("coefficients")) %>%
map_dbl(8) %>%
tidy %>%
arrange(desc(x)) %>%
rename(p.value = x) -> modell.pvalue
## Warning: 'tidy.numeric' ist veraltet.
## Siehe help("Deprecated")
kable(modell.pvalue)
names | p.value |
---|---|
education | 0.9524501 |
gender | 0.7740138 |
occupation | 0.2245709 |
age | 0.0195320 |
children | 0.0107275 |
religiousness | 0.0003797 |
yearsmarried | 0.0000040 |
rating | 0.0000000 |
Eine Gegenüberstellung der Ergebnisse kann folgendermaßen aussehen:
cbind(modell, modell.pvalue)
## names r.squared names p.value
## 1 rating 7.812718e-02 education 9.524501e-01
## 2 yearsmarried 3.490982e-02 gender 7.740138e-01
## 3 religiousness 2.088064e-02 occupation 2.245709e-01
## 4 children 1.081809e-02 age 1.953203e-02
## 5 age 9.070125e-03 children 1.072749e-02
## 6 occupation 2.461327e-03 religiousness 3.797263e-04
## 7 gender 1.377396e-04 yearsmarried 3.995520e-06
## 8 education 5.941117e-06 rating 3.002385e-12
Dies ist allerdings sehr unschön, da die Reihenfolge der beiden Listen mit absteigenden R2 bzw p-Wert angefordert wurden und daher unterschiedlich angeordnet sind. Abhilfe leistet hier der left_join Befehl.
Gute_Ansicht <- modell %>% left_join(modell.pvalue, by = "names") %>%
mutate(P_Wert = round(p.value, digits = 2)) %>%
select(-p.value)
Gute_Ansicht
## # A tibble: 8 x 3
## names r.squared P_Wert
## <chr> <dbl> <dbl>
## 1 rating 0.0781 0
## 2 yearsmarried 0.0349 0
## 3 religiousness 0.0209 0
## 4 children 0.0108 0.01
## 5 age 0.00907 0.02
## 6 occupation 0.00246 0.22
## 7 gender 0.000138 0.77
## 8 education 0.00000594 0.95
6 Conclusion
Die Verwendung der map Funktion ist mit ein wenig Übung leicht anzuwenden und unglaublich effektiv, wenn es um automatisierte Datenanalyse geht.