Data Science, Machine Learning und KI
Kontakt

Wir bei statworx arbeiten viel mit R und verwenden oft die gleichen kleinen Hilfsfunktionen in unseren Projekten. Diese Funktionen erleichtern unseren Arbeitsalltag, indem sie sich-wiederholende Codeteile reduzieren oder Übersichten über unsere Projekte erstellen.

Um diese Funktionen innerhalb unserer Teams und auch mit anderen zu teilen, habe ich angefangen, sie zu sammeln und habe dann daraus ein R-Paket namens helfRlein erstellt. Neben der gemeinsamen Nutzung wollte ich auch einige Anwendungsfälle haben, um meine Fähigkeiten zur Fehlersuche und Optimierung zu verbessern. Mit der Zeit wuchs das Paket und es kamen immer mehr Funktionen zusammen. Beim letzten Mal habe ich jede Funktion als Teil eines Adventskalenders vorgestellt. Zum Start unserer neuen Website habe ich alle Funktionen in diesem Kalender zusammengefasst und werde jede aktuelle Funktion aus dem Paket helfRlein vorstellen.

Die meisten Funktionen wurden entwickelt, als es ein Problem gab und man eine einfache Lösung dafür brauchte. Zum Beispiel war der angezeigte Text zu lang und musste gekürzt werden (siehe evenstrings). Andere Funktionen existieren nur, um sich-wiederholende Aufgaben zu reduzieren – wie das Einlesen mehrerer Dateien des selben Typs (siehe read_files). Daher könnten diese Funktionen auch für Euch nützlich sein!

Um alle Funktionen im Detail zu erkunden, könnt Ihr unser GitHub besuchen. Wenn Ihr irgendwelche Vorschläge habt, schickt mir bitte eine E-Mail oder öffnet ein Issue auf GitHub!

1. char_replace

Dieser kleine Helfer ersetzt Sonderzeichen (wie z. B. den Umlaut „ä“) durch ihre Standardentsprechung (in diesem Fall „ae“). Es ist auch möglich, alle Zeichen in Kleinbuchstaben umzuwandeln, Leerzeichen zu entfernen oder Leerzeichen und Bindestriche durch Unterstriche zu ersetzen.

Schauen wir uns ein kleines Beispiel mit verschiedenen Settings an:

x <- " Élizàldë-González Strasse"
char_replace(x, to_lower = TRUE)
[1] "elizalde-gonzalez strasse"
char_replace(x, to_lower = TRUE, to_underscore = TRUE)
[1] "elizalde_gonzalez_strasse"
char_replace(x, to_lower = FALSE, rm_space = TRUE, rm_dash = TRUE)
[1] "ElizaldeGonzalezStrasse"

2. checkdir

Dieser kleine Helfer prüft einen gegebenen Ordnerpfad auf Existenz und erstellt ihn bei Bedarf.

checkdir(path = "testfolder/subfolder")

Intern gibt es nur eine einfache if-Anweisung, die die R-Basisfunktionen file.exists() und dir.create(). kombiniert.

3. clean_gc

Dieser kleine Helfer gibt den Speicher von unbenutzten Objekten frei. Nun, im Grunde ruft es einfach gc() ein paar Mal auf. Ich habe das vor einiger Zeit für ein Projekt benutzt, bei dem ich mit riesigen Datendateien gearbeitet habe. Obwohl wir das Glück hatten, einen großen Server mit 500 GB RAM zu haben, stießen wir bald an seine Grenzen. Da wir in der Regel mehrere Prozesse parallelisieren, mussten wir jedes Bit und jedes Byte des Arbeitsspeichers nutzen, das wir bekommen konnten. Anstatt also viele Zeilen wie diese zu haben:

gc();gc();gc();gc()

… habe ich clean_gc() der Einfachheit halber geschrieben. Intern wird gc() so lange aufgerufen, wie es Speicher gibt, der freigegeben werden muss.

Some further thoughts

Es gibt einige Diskussionen über den Garbage Collector gc() und seine Nützlichkeit. Wenn Ihr mehr darüber erfahren wollt, schlage ich vor, dass Ihr Euch die memory section in Advanced R anseht. Ich weiß, dass R selbst bei Bedarf Speicher freigibt, aber ich bin mir nicht sicher, was passiert, wenn Ihr mehrere R-Prozesse habt. Können sie den Speicher von anderen Prozessen leeren? Wenn Ihr dazu etwas mehr wisst, lasst es mich wissen!

4. count_na

Dieser kleine Helfer zählt fehlende Werte innerhalb eines Vektors.

x <- c(NA, NA, 1, NaN, 0)
count_na(x)
3

Intern gibt es nur ein einfaches sum(is.na(x)), das die NA-Werte zählt. Wenn Ihr den Mittelwert statt der Summe wollt, könnt Ihr prop = TRUE setzen.

5. evenstrings

Dieser kleine Helfer zerlegt eine gegebene Zeichenkette in kleinere Teile mit einer festen Länge. Aber warum? Nun, ich brauchte diese Funktion beim Erstellen eines Plots mit einem langen Titel. Der Text war zu lang für eine Zeile und anstatt ihn einfach abzuschneiden oder über die Ränder laufen zu lassen, wollte ich ihn schön trennen.

Bei einer langen Zeichenkette wie…

long_title <- c("Contains the months: January, February, March, April, May, June, July, August, September, October, November, December")

…wollen wir sie nach split = "," mit einer maximalen Länge von char = 60 aufteilen.

short_title <- evenstrings(long_title, split = ",", char = 60)

Die Funktion hat zwei mögliche Ausgabeformate, die durch Setzen von newlines = TRUE oder FALSE gewählt werden können:

  • eine Zeichenkette mit Zeilentrennzeichen \n
  • ein Vektor mit jedem Unterteil.

Ein anderer Anwendungsfall könnte eine Nachricht sein, die mit cat() auf der Konsole ausgegeben wird:

cat(long_title)
Contains the months: January, February, March, April, May, June, July, August, September, October, November, December
cat(short_title)
Contains the months: January, February, March, April, May,
 June, July, August, September, October, November, December

Code for plot example

p1 <- ggplot(data.frame(x = 1:10, y = 1:10),
  aes(x = x, y = y)) +
  geom_point() +
  ggtitle(long_title)

p2 <- ggplot(data.frame(x = 1:10, y = 1:10),
  aes(x = x, y = y)) +
  geom_point() +
  ggtitle(short_title)

multiplot(p1, p2)

6. get_files

Dieser kleine Helfer macht das Gleiche wie die „Find in files „ Suche in RStudio. Sie gibt einen Vektor mit allen Dateien in einem bestimmten Ordner zurück, die das Suchmuster enthalten. In Eurem täglichen Arbeitsablauf würdet Ihr normalerweise die Tastenkombination SHIFT+CTRL+F verwenden. Mit get_files() könnt Ihr diese Funktionen in Euren Skripten nutzen.

7. get_network

Das Ziel dieses kleinen Helfers ist es, die Verbindungen zwischen R-Funktionen innerhalb eines Projekts als Flussdiagramm zu visualisieren. Dazu ist die Eingabe ein Verzeichnispfad zur Funktion oder eine Liste mit den Funktionen und die Ausgaben sind eine Adjazenzmatrix und ein Graph-Objekt. Als Beispiel verwenden wir diesen Ordner mit einigen Spielzeugfunktionen:

net <- get_network(dir = "flowchart/R_network_functions/", simplify = FALSE)
g1 <- net$igraph

Input

Es gibt fünf Parameter, um mit der Funktion zu interagieren:

  • ein Pfad dir, der durchsucht werden soll.
  • ein Zeichenvektor Variationen mit der Definitionszeichenfolge der Funktion – die Vorgabe ist c(" <- function", "<- function", "<-function").
  • ein „Muster“, eine Zeichenkette mit dem Dateisuffix – die Vorgabe ist "\\.R$".
  • ein boolesches simplify, das Funktionen ohne Verbindungen aus der Darstellung entfernt.
  • eine benannte Liste all_scripts, die eine Alternative zu dir ist. Diese Liste wird hauptsächlich nur zu Testzwecken verwendet.

Für eine normale Verwendung sollte es ausreichen, einen Pfad zum Projektordner anzugeben.

Output

Der gegebene Plot zeigt die Verbindungen der einzelnen Funktionen (Pfeile) und auch die relative Größe des Funktionscodes (Größe der Punkte). Wie bereits erwähnt, besteht die Ausgabe aus einer Adjazenzmatrix und einem Graph-Objekt. Die Matrix enthält die Anzahl der Aufrufe für jede Funktion. Das Graph-Objekt hat die folgenden Eigenschaften:

  • Die Namen der Funktionen werden als Label verwendet.
  • Die Anzahl der Zeilen jeder Funktion (ohne Kommentare und Leerzeilen) wird als Größe gespeichert.
  • Der Ordnername des ersten Ordners im Verzeichnis.
  • Eine Farbe, die dem Ordner entspricht.

Mit diesen Eigenschaften können Sie die Netzwerkdarstellung zum Beispiel wie folgt verbessern:

library(igraph)

# create plots ------------------------------------------------------------
l <- layout_with_fr(g1)
colrs <- rainbow(length(unique(V(g1)$color)))

plot(g1,
     edge.arrow.size = .1,
     edge.width = 5*E(g1)$weight/max(E(g1)$weight),
     vertex.shape = "none",
     vertex.label.color = colrs[V(g1)$color],
     vertex.label.color = "black",
     vertex.size = 20,
     vertex.color = colrs[V(g1)$color],
     edge.color = "steelblue1",
     layout = l)
legend(x = 0,
       unique(V(g1)$folder), pch = 21,
       pt.bg = colrs[unique(V(g1)$color)],
       pt.cex = 2, cex = .8, bty = "n", ncol = 1)
 

example-network-helfRlein

8. get_sequence

Dieser kleine Helfer gibt Indizes von wiederkehrenden Mustern zurück. Es funktioniert sowohl mit Zahlen als auch mit Zeichen. Alles, was es braucht, ist ein Vektor mit den Daten, ein Muster, nach dem gesucht werden soll, und eine Mindestanzahl von Vorkommen.

Lasst uns mit dem folgenden Code einige Zeitreihendaten erstellen.

library(data.table)

# random seed
set.seed(20181221)

# number of observations
n <- 100

# simulationg the data
ts_data <- data.table(DAY = 1:n, CHANGE = sample(c(-1, 0, 1), n, replace = TRUE))
ts_data[, VALUE := cumsum(CHANGE)]

Dies ist nichts anderes als ein Random Walk, da wir zwischen dem Abstieg (-1), dem Anstieg (1) und dem Verbleib auf demselben Niveau (0) wählen. Unsere Zeitreihendaten sehen folgendermaßen aus:

Angenommen, wir wollen die Datumsbereiche wissen, in denen es an mindestens vier aufeinanderfolgenden Tagen keine Veränderung gab.

ts_data[, get_sequence(x = CHANGE, pattern = 0, minsize = 4)]
     min max
[1,]  45  48
[2,]  65  69

Wir können auch die Frage beantworten, ob sich das Muster „down-up-down-up“ irgendwo wiederholt:

ts_data[, get_sequence(x = CHANGE, pattern = c(-1,1), minsize = 2)]
     min max
[1,]  88  91

Mit diesen beiden Eingaben können wir unseren Plot ein wenig aktualisieren, indem wir etwas geom_rect hinzufügen!

Code for the plot

rect <- data.table(
  rbind(ts_data[, get_sequence(x = CHANGE, pattern = c(0), minsize = 4)],
        ts_data[, get_sequence(x = CHANGE, pattern = c(-1,1), minsize = 2)]),
  GROUP = c("no change","no change","down-up"))

ggplot(ts_data, aes(x = DAY, y = VALUE)) +
  geom_line() +
  geom_rect(data = rect,
  inherit.aes = FALSE,
  aes(xmin = min - 1,
  xmax = max,
  ymin = -Inf,
  ymax = Inf,
  group = GROUP,
  fill = GROUP),
  color = "transparent",
  alpha = 0.5) +
  scale_fill_manual(values = statworx_palette(number = 2, basecolors = c(2,5))) +
  theme_minimal()

9. intersect2

Dieser kleine Helfer gibt den Schnittpunkt mehrerer Vektoren oder Listen zurück. Ich habe diese Funktion hier gefunden, fand sie recht nützlich und habe sie ein wenig angepasst.

intersect2(list(c(1:3), c(1:4)), list(c(1:2),c(1:3)), c(1:2))
[1] 1 2

Intern wird das Problem, die Schnittmenge zu finden, rekursiv gelöst, wenn ein Element eine Liste ist, und dann schrittweise mit dem nächsten Element.

10. multiplot

Dieses kleine Hilfsmittel kombiniert mehrere ggplots zu einem Plot. Dies ist eine Funktion aus dem R-cookbook.

Ein Vorteil gegenüber facets ist, dass man nicht alle Daten für alle Plots in einem Objekt benötigt. Auch kann man jeden einzelnen Plot frei erstellen – was manchmal auch ein Nachteil sein kann.

Mit dem Parameter layout könnt Ihr mehrere Plots mit unterschiedlichen Größen anordnen. Nehmen wir an, Ihr habt drei Plots und wollt sie wie folgt anordnen:

1    2    2
1    2    2
3    3    3

Bei multiplot läuft es auf Folgendes hinaus:

multiplot(plotlist = list(p1, p2, p3),
          layout = matrix(c(1,2,2,1,2,2,3,3,3), nrow = 3, byrow = TRUE))

Code for plot example

# star coordinates
c1  =   cos((2*pi)/5)   
c2  =   cos(pi/5)
s1  =   sin((2*pi)/5)
s2  =   sin((4*pi)/5)

data_star <- data.table(X = c(0, -s2, s1, -s1, s2),
                        Y = c(1, -c2, c1, c1, -c2))

p1 <- ggplot(data_star, aes(x = X, y = Y)) +
  geom_polygon(fill = "gold") +
  theme_void()

# tree
set.seed(24122018)
n <- 10000
lambda <- 2
data_tree <- data.table(X = c(rpois(n, lambda), rpois(n, 1.1*lambda)),
                        TYPE = rep(c("1", "2"), each = n))
data_tree <- data_tree[, list(COUNT = .N), by = c("TYPE", "X")]
data_tree[TYPE == "1", COUNT := -COUNT]

p2 <- ggplot(data_tree, aes(x = X, y = COUNT, fill = TYPE)) +
  geom_bar(stat = "identity") +
  scale_fill_manual(values = c("green", "darkgreen")) +
  coord_flip() +
  theme_minimal()

# gifts
data_gifts <- data.table(X = runif(5, min = 0, max = 10),
                         Y = runif(5, max = 0.5),
                         Z = sample(letters[1:5], 5, replace = FALSE))

p3 <- ggplot(data_gifts, aes(x = X, y = Y)) +
  geom_point(aes(color = Z), pch = 15, size = 10) +
  scale_color_brewer(palette = "Reds") +
  geom_point(pch = 12, size = 10, color = "gold") +
  xlim(0,8) +
  ylim(0.1,0.5) +
  theme_minimal() + 
  theme(legend.position="none") 


11. na_omitlist

Dieser kleine Helfer entfernt fehlende Werte aus einer Liste.

y <- list(NA, c(1, NA), list(c(5:6, NA), NA, "A"))

Es gibt zwei Möglichkeiten, die fehlenden Werte zu entfernen, entweder nur auf der ersten Ebene der Liste oder innerhalb jeder Unterebene.

na_omitlist(y, recursive = FALSE)
[[1]]
[1]  1 NA

[[2]]
[[2]][[1]]
[1]  5  6 NA

[[2]][[2]]
[1] NA

[[2]][[3]]
[1] "A"
na_omitlist(y, recursive = TRUE)
[[1]]
[1] 1

[[2]]
[[2]][[1]]
[1] 5 6

[[2]][[2]]
[1] "A"

12. %nin%

Dieser kleine Helfer ist eine reine Komfortfunktion. Sie ist einfach dasselbe wie der negierte %in%-Operator, wie Ihr unten sehen könnt. Aber meiner Meinung nach erhöht sie die Lesbarkeit des Codes.

all.equal( c(1,2,3,4) %nin% c(1,2,5),
          !c(1,2,3,4) %in%  c(1,2,5))
[1] TRUE

Dieser Operator hat es auch in einige andere Pakete geschafft – wie Ihr hier nachlesen könnt.

13. object_size_in_env

Dieser kleine Helfer zeigt eine Tabelle mit der Größe jedes Objekts in der vorgegebenen Umgebung an.

Wenn Ihr in einer Situation seid, in der Ihr viel gecodet habt und Eure Umgebung nun ziemlich unübersichtlich ist, hilft Euch object_size_in_env, die großen Fische in Bezug auf den Speicherverbrauch zu finden. Ich selbst bin ein paar Mal auf dieses Problem gestoßen, als ich mehrere Ausführungen meiner Modelle in einem Loop durchlaufen habe. Irgendwann wurden die Sitzungen ziemlich groß im Speicher und ich wusste nicht, warum! Mit Hilfe von object_size_in_env und etwas Degubbing konnte ich das Objekt ausfindig machen, das dieses Problem verursachte, und meinen Code entsprechend anpassen.

Zuerst wollen wir eine Umgebung mit einigen Variablen erstellen.

# building an environment
this_env <- new.env()
assign("Var1", 3, envir = this_env)
assign("Var2", 1:1000, envir = this_env)
assign("Var3", rep("test", 1000), envir = this_env)

Um die Größeninformationen unserer Objekte zu erhalten, wird intern format(object.size()) verwendet. Mit der Einheit kann das Ausgabeformat geändert werden (z.B. "B", "MB" oder "GB").

# checking the size
object_size_in_env(env = this_env, unit = "B")
   OBJECT SIZE UNIT
1:   Var3 8104    B
2:   Var2 4048    B
3:   Var1   56    B

14. print_fs

Dieser kleine Helfer gibt die Ordnerstruktur eines gegebenen Pfades zurück. Damit kann man z.B. eine schöne Übersicht in die Dokumentation eines Projektes oder in ein Git einbauen. Im Sinne der Automatisierung könnte diese Funktion nach einer größeren Änderung Teile in einer Log- oder News-Datei ändern.

Wenn wir uns das gleiche Beispiel anschauen, das wir für die Funktion get_network verwendet haben, erhalten wir folgendes:

print_fs("~/flowchart/", depth = 4)
1  flowchart                            
2   ¦--create_network.R                 
3   ¦--getnetwork.R                     
4   ¦--plots                            
5   ¦   ¦--example-network-helfRlein.png
6   ¦   °--improved-network.png         
7   ¦--R_network_functions              
8   ¦   ¦--dataprep                     
9   ¦   ¦   °--foo_01.R                 
10  ¦   ¦--method                       
11  ¦   ¦   °--foo_02.R                 
12  ¦   ¦--script_01.R                  
13  ¦   °--script_02.R                  
14  °--README.md 

Mit depth können wir einstellen, wie tief wir unsere Ordner durchforsten wollen.

15. read_files

Dieser kleine Helfer liest mehrere Dateien des selben Typs ein und fasst sie zu einer data.table zusammen. Welche Art von Dateilesefunktion verwendet werden soll, kann mit dem Argument FUN ausgewählt werden.

Wenn Sie eine Liste von Dateien haben, die alle mit der gleichen Funktion eingelesen werden sollen (z.B. read.csv), können Sie statt lapply und rbindlist nun dies verwenden:

read_files(files, FUN = readRDS)
read_files(files, FUN = readLines)
read_files(files, FUN = read.csv, sep = ";")

Intern verwendet es nur lapply und rbindlist, aber man muss es nicht ständig eingeben. Die read_files kombiniert die einzelnen Dateien nach ihren Spaltennamen und gibt eine data.table zurück. Warum data.table? Weil ich es mag. Aber lassen Sie uns nicht das Fass von data.table vs. dplyr aufmachen (zum Fass…).

16. save_rds_archive

Dieser kleine Helfer ist ein Wrapper um die Basis-R-Funktion saveRDS() und prüft, ob die Datei, die Ihr zu speichern versucht, bereits existiert. Wenn ja, wird die bestehende Datei umbenannt / archiviert (mit einem Zeitstempel), und die „aktualisierte“ Datei wird unter dem angegebenen Namen gespeichert. Das bedeutet, dass vorhandener Code, der davon abhängt, dass der Dateiname konstant bleibt (z.B. readRDS()-Aufrufe in anderen Skripten), weiterhin funktionieren wird, während eine archivierte Kopie der – ansonsten überschriebenen – Datei erhalten bleibt.

17. sci_palette

Dieser kleine Helfer liefert eine Reihe von Farben, die wir bei statworx häufig verwenden. Wenn Ihr Euch also – so wie ich – nicht an jeden Hex-Farbcode erinnern könnt, den Ihr braucht, könnte das helfen. Natürlich sind das unsere Farben, aber Ihr könnt es auch mit Eurer eigenen Farbpalette umschreiben. Aber der Hauptvorteil ist die Plot-Methode – so könnt Ihr die Farbe sehen, anstatt nur den Hex-Code zu lesen.

So seht Ihr, welcher Hexadezimalcode welcher Farbe entspricht und wofür Ihr ihn verwenden könnt.

sci_palette(scheme = "new")
Tech Blue       Black       White  Light Grey    Accent 1    Accent 2    Accent 3 
"#0000FF"   "#000000"   "#FFFFFF"   "#EBF0F2"   "#283440"   "#6C7D8C"   "#B6BDCC"   
Highlight 1 Highlight 2 Highlight 3 
"#00C800"   "#FFFF00"   "#FE0D6C" 
attr(,"class")
[1] "sci"

Wie bereits erwähnt, gibt es eine Methode plot(), die das folgende Bild ergibt.

plot(sci_palette(scheme = "new"))

18. statusbar

Dieser kleine Helfer gibt einen Fortschrittsbalken in der Konsole für Schleifen aus.

Es gibt zwei notwendige Parameter, um diese Funktion zu füttern:

  • run ist entweder der Iterator oder seine Nummer
  • max.run ist entweder alle möglichen Iteratoren in der Reihenfolge, in der sie verarbeitet werden, oder die maximale Anzahl von Iterationen.

So könnte es zum Beispiel run = 3 und max.run = 16 oder run = "a" und max.run = Buchstaben[1:16] sein.

Außerdem gibt es zwei optionale Parameter:

  • percent.max beeinflusst die Breite des Fortschrittsbalkens
  • info ist ein zusätzliches Zeichen, das am Ende der Zeile ausgegeben wird. Standardmäßig ist es run.

Ein kleiner Nachteil dieser Funktion ist, dass sie nicht mit parallelen Prozessen arbeitet. Wenn Ihr einen Fortschrittsbalken haben wollt, wenn Ihr apply Funktionen benutzt, schaut Euch pbapply an.

19. statworx_palette

Dieses kleine Hilfsmittel ist eine Ergänzung zu sci_palette(). Wir haben die Farben 1, 2, 3, 5 und 10 ausgewählt, um eine flexible Farbpalette zu erstellen. Wenn Sie 100 verschiedene Farben benötigen – sagen Sie nichts mehr!

Im Gegensatz zu sci_palette() ist der Rückgabewert ein Zeichenvektor. Zum Beispiel, wenn Sie 16 Farben wollen:

statworx_palette(16, scheme = "old")
[1] "#013848" "#004C63" "#00617E" "#00759A" "#0087AB" "#008F9C" "#00978E" "#009F7F"
[9] "#219E68" "#659448" "#A98B28" "#ED8208" "#F36F0F" "#E45A23" "#D54437" "#C62F4B"

Wenn wir nun diese Farben aufzeichnen, erhalten wir einen schönen regenbogenartigen Farbverlauf.

library(ggplot2)

ggplot(plot_data, aes(x = X, y = Y)) +
  geom_point(pch = 16, size = 15, color = statworx_palette(16, scheme = "old")) +
  theme_minimal()

Eine zusätzliche Funktion ist der Parameter reorder, der die Reihenfolge der Farben abtastet, so dass Nachbarn vielleicht etwas besser unterscheidbar sind. Auch wenn Sie die verwendeten Farben ändern wollen, können Sie dies mit basecolors tun.

ggplot(plot_data, aes(x = X, y = Y)) +
  geom_point(pch = 16, size = 15,
             color = statworx_palette(16, basecolors = c(4,8,10), scheme = "new")) +
  theme_minimal()


20. strsplit

Dieses kleine Hilfsmittel erweitert die R-Basisfunktion strsplit – daher der gleiche Name! Es ist nun möglich, before, after oder between ein bestimmtes Begrenzungszeichen zu trennen. Im Falle von between müsst ihr zwei Delimiter angeben.

Eine frühere Version dieser Funktion findet Ihr in diesem Blogbeitrag, wo ich die verwendeten regulären Ausdrücke beschreibe, falls Ihr daran interessiert seid.

Hier ist ein kleines Beispiel, wie man das neue strsplit benutzt.

text <- c("This sentence should be split between should and be.")

strsplit(x = text, split = " ")
strsplit(x = text, split = c("should", " be"), type = "between")
strsplit(x = text, split = "be", type = "before")
[[1]]
[1] "This"     "sentence" "should"   "be"       "split"    "between"  "should"   "and"     
[9] "be."

[[1]]
[1] "This sentence should"             " be split between should and be."

[[1]]
[1] "This sentence should " "be split "             "between should and "  
[4] "be."

21. to_na

Dieser kleine Helfer ist nur eine Komfortfunktion. Bei der Datenaufbereitung kann es vorkommen, dass Ihr einen Vektor mit unendlichen Werten wie Inf oder -Inf oder sogar NaN-Werten habt. Solche Werte können (müssen aber nicht!) Eure Auswertungen und Modelle durcheinanderbringen. Aber die meisten Funktionen haben die Tendenz, fehlende Werte zu behandeln. Daher entfernt diese kleine Hilfe solche Werte und ersetzt sie durch NA.

Ein kleines Beispiel, um Euch die Idee zu vermitteln:

test <- list(a = c("a", "b", NA),
             b = c(NaN, 1,2, -Inf),
             c = c(TRUE, FALSE, NaN, Inf))

lapply(test, to_na)
$a
[1] "a" "b" NA 

$b
[1] NA  1  2 NA

$c
[1]  TRUE FALSE    NA

Ein kleiner Tipp am Rande! Da es je nach den anderen Werten innerhalb eines Vektors verschiedene Arten von NA gibt, solltet Ihr das Format überprüfen, wenn Ihr to_na auf Gruppen oder Teilmengen anwendet.

test <- list(NA, c(NA, "a"), c(NA, 2.3), c(NA, 1L))
str(test)
List of 4
 $ : logi NA
 $ : chr [1:2] NA "a"
 $ : num [1:2] NA 2.3
 $ : int [1:2] NA 1

22. trim

Dieser kleine Helfer entfernt führende und nachfolgende Leerzeichen aus einer Zeichenkette. Mit R Version 3.5.1 wurde trimws eingeführt, das genau das Gleiche tut. Das zeigt nur, dass es keine schlechte Idee war, eine solche Funktion zu schreiben. 😉

x <- c("  Hello world!", "  Hello world! ", "Hello world! ")
trim(x, lead = TRUE, trail = TRUE)
[1] "Hello world!" "Hello world!" "Hello world!"

Die Parameter lead und trail geben an, ob nur die führenden, die nachfolgenden oder beide Leerzeichen entfernt werden sollen.

Fazit

Ich hoffe, dass euch das helfRlein Package genauso die Arbeit erleichtert, wie uns hier bei statworx. Schreibt uns bei Fragen oder Input zum Package gerne eine Mail an: blog@statworx.com

Jakob Gepp Jakob Gepp Jakob Gepp Jakob Gepp Jakob Gepp Jakob Gepp

Im Bereich Data Science – wie der Name schon sagt – ist das Thema Daten, vom Data Cleaning bis hin zum Feature Engineering, einer der Grundpfeiler. Daten zu haben und auszuwerten ist die eine Seite, doch wie kommt man eigentlich an Daten für neue Problemstellungen?

Wenn man Glück hat, werden die Daten, die man benötigt, bereits zur Verfügung gestellt. Sei es über den Download eines ganzen Datensatzes oder die Verwendung einer API. Häufig muss man allerdings auch Informationen von Webseiten selbst zusammentragen – das nennt man Web Scraping. Je nachdem wie oft man Daten scrapen will, ist es von Vorteil, diesen Schritt zu automatisieren.

In diesem Beitrag soll es genau um diese Automatisierung gehen. Ich werde mittels Web Scraping und GitHub Actions an einem Beispiel aufzeigen, wie man sich selbst Datensätze über einen längeren Zeitraum erstellen kann. Dabei soll der Fokus auf den Erfahrungen liegen, die ich in den letzten Monaten gesammelt habe.

Der verwendete Code sowie die bisher gesammelten Daten befinden sich in diesem GitHub Repo.

Suche nach Daten – Ausgangslage

Bei meiner Recherche für den Blogbeitrag über die Benzinpreise, bin ich auch über Daten zur Auslastung der Parkhäuser in Frankfurt am Main gestoßen. Die Beschaffung dieser Daten legte den Grundstein für diesen Beitrag. Nach einigen Überlegungen und zusätzlicher Recherche kamen mir noch weitere thematisch passende Datenquellen in den Sinn:

  • Auslastung der Straßen
  • Verspätungen der S- und U-Bahnen
  • Events in der Nähe
  • Wetterdaten

Schnell stellte sich jedoch heraus, dass ich nicht alle diese Daten bekommen konnte, da sie nicht frei verfügbar sind bzw. es nicht gestattet ist, diese zu speichern. Da ich vorhatte, die gesammelten Daten auf GitHub zu speichern und verfügbar zu machen, war dies ein entscheidender Punkt, welche Daten in Frage kamen. Aus diesen Gründen fielen die Bahndaten vollkommen raus. Für die Straßenauslastung habe ich lediglich Daten für Köln gefunden und ich wollte es vermeiden, die Google API zu nutzen, da das durchaus seine eigenen Herausforderungen mit sich bringt. Es blieben also Event- und Wetterdaten.

Für die Wetterdaten des Deutschen Wetterdienstes kann das rdwd Packet genutzt werden. Da diese Daten bereits historisiert vorliegen, sind sie für diesen Blogbeitrag nebensächlich. Um an die verbleibenden Event- und Parkdaten zu kommen, haben sich die GitHub Actions als sehr nützlich erwiesen – auch wenn sie nicht ganz trivial in der Anwendung sind. Besonders der Umstand, dass diese kostenfrei genutzt werden können, machen sie zu einem empfehlenswerten Tool für solche Projekte.

Scrapen der Daten

Da sich dieser Beitrag nicht mit Details zum Thema Webscraping befassen wird, verweise ich an dieser Stelle auf den Beitrag von meinem Kollegen David.

Die Parkdaten stehen hier im XML-Format bereit und werden alle fünf Minuten aktualisiert. Sobald man die Struktur des XML verstanden hat, müsst ihr nur noch auf den richtigen Index zugreifen und ihr habt die Daten, die ihr möchtet.

In der Funktion get_parking_data() habe ich alles zusammengefasst, was ich benötige. Es wird ein Datensatz zur Area und ein Datensatz zu den einzelnen Parkhäusern erstellt.

Beispiel Datenauszug area

parkingAreaOccupancy;parkingAreaStatusTime;parkingAreaTotalNumberOfVacantParkingSpaces;
totalParkingCapacityLongTermOverride;totalParkingCapacityShortTermOverride;id;TIME
0.08401977;2021-12-01T01:07:00Z;556;150;607;1[Anlagenring];2021-12-01T01:07:02.720Z
0.31417114;2021-12-01T01:07:00Z;513;0;748;4[Bahnhofsviertel];2021-12-01T01:07:02.720Z
0.351417;2021-12-01T01:07:00Z;801;0;1235;5[Dom / Römer];2021-12-01T01:07:02.720Z
0.21266666;2021-12-01T01:07:00Z;1181;70;1500;2[Zeil];2021-12-01T01:07:02.720Z

Beispiel Datenauszug facility

parkingFacilityOccupancy;parkingFacilityStatus;parkingFacilityStatusTime;
totalNumberOfOccupiedParkingSpaces;totalNumberOfVacantParkingSpaces;
totalParkingCapacityLongTermOverride;totalParkingCapacityOverride;
totalParkingCapacityShortTermOverride;id;TIME
0.02;open;2021-12-01T01:02:00Z;4;196;150;350;200;24276[Turmcenter];2021-12-01T01:07:02.720Z
0.11547912;open;2021-12-01T01:02:00Z;47;360;0;407;407;18944[Alte Oper];2021-12-01T01:07:02.720Z
0.0027472528;open;2021-12-01T01:02:00Z;1;363;0;364;364;24281[Hauptbahnhof Süd];2021-12-01T01:07:02.720Z
0.609375;open;2021-12-01T01:02:00Z;234;150;0;384;384;105479[Baseler Platz];2021-12-01T01:07:02.720Z

Für die Eventdaten scrape ich die Seite stadtleben.de. Da es sich um eine HTML handelt, die recht gut strukturiert ist, kann ich über den Tag „kalenderListe“ auf die tabellarische Eventübersicht zugreifen. Das Resultat wird durch die Funktion get_event_data() erstellt.

Beispiel Datenauszug events

eventtitle;views;place;address;eventday;eventdate;request
Magical Sing Along - Das lustigste Mitsing-Event;12576;Bürgerhaus;64546 Mörfelden-Walldorf, Westendstraße 60;Freitag;2022-03-04;2022-03-04T02:24:14.234833Z
Velvet-Bar-Night;1460;Velvet Club;60311 Frankfurt, Weißfrauenstraße 12-16;Freitag;2022-03-04;2022-03-04T02:24:14.234833Z
Basta A-cappella-Band;465;Zeltpalast am Deutsche Bank Park;60528 Frankfurt am Main, Mörfelder Landstraße 362;Freitag;2022-03-04;2022-03-04T02:24:14.234833Z
BeThrifty Vintage Kilo Sale | Frankfurt | 04. & 05. …;1302;Batschkapp;60388 Frankfurt am Main, Gwinnerstraße 5;Freitag;2022-03-04;2022-03-04T02:24:14.234833Z

Automation der Abläufe – GitHub Actions

Das Grundgerüst steht. Ich habe je eine Funktion, die mir die Park- und Eventdaten beim Ausführen in eine .csv Datei schreiben. Da ich die Parkdaten alle fünf Minuten und die Eventdaten zur Sicherheit drei Mal am Tag abfragen möchte, kommen nun GitHub Actions ins Spiel.

Mit dieser Funktion von GitHub können neben Aktionen, die beim Mergen oder Committen auslösen, auch Workflows zeitlich geplant und durchgeführt werden. Hierfür wird eine .yml Datei im Order /.github/workflows erstellt.

Die Hauptbestandteile meines Workflows sind:

  • Der schedule – Alle zehn Minuten sollen die Funktionen ausgeführt werden.
  • Das OS – Da ich lokal auf einem Mac entwickle, nutze ich hier das macOS-latest.
  • Umgebungsvariablen – Hier ist neben meinem GitHub Token auch der Pfad für das Paketmanagement renv enthalten
  • Die einzelnen steps im Workflow selbst

Der Workflow durchläuft die folgenden Schritte:

  • Setup R
  • Pakete laden mit renv
  • Script ausführen um Daten zu scrapen
  • Script ausführen um die README zu aktualisieren
  • Pushen der neuen Daten zurück ins git

Jeder dieser Schritte ist an sich sehr klein und übersichtlich, jedoch liegt der Teufel wie so oft im Detail.

Limitation und Herausforderungen

Im Laufe der letzten Monate habe ich meinen Workflow immer wieder angepasst und optimiert, um den aufkommenden Fehlern und Problemen Herr zu werden. Nachfolgend also der Überblick über meine kondensierten Erfahrungen mit GitHub Actions.

Schedule Probleme

Wer zeitkritische Aktionen durchführen möchte, sollte auf andere Services zugreifen. GitHub Actions garantieren einem nicht, dass die Jobs exakt getimed werden (oder teilweise überhaupt durchgeführt werden). In der Tabelle sind die Zeiten zwischen zwei erfolgreichen Abfragen angegeben.

Zeitspanne in Minuten <= 5 <= 10 <= 20 <= 60 > 60
Anzahl Abfragen 1720 2049 5509 3023 194

Man sieht, dass die geplanten fünf Minuten Intervalle nicht immer eingehalten wurden. Hier sollte ich in Zukunft einen größeren Spielraum einplanen.

Merge Konflikte

Zu Beginn hatte ich zwei Workflows, einen für die Parkdaten und einen für die Events. Wenn diese sich zeitlich überlappt haben, dann kam es zu Merge-Konflikten, da beide Prozesse die README mit einen Zeitstempel updaten. Im Verlauf bin ich umgestiegen auf einen Workflow samt Errorhandling.
Auch wenn ein Durchlauf länger gedauert hat und der nächste bereits gestartet wurde, kam es beim Pushen zu Merge-Konflikten in den .csv-Daten. Lange Durchläufe entstanden häufig durch das R Setup und das Laden der packages. Als Konsequenz habe ich das Schedule-Intervall von fünf auf zehn Minuten erweitert.

Formatanpassungen

Es gab ein paar Situationen, in denen sich die Pfade oder Struktur der gescrapten Daten geändert haben, so dass ich meine Funktionen anpassen musste. Hierbei war die Einstellung, eine E-Mail zu bekommen, falls ein Prozess gescheitert ist, sehr hilfreich.

Fehlende Testmöglichkeiten

Es gibt bisher keine andere Möglichkeit ein Workflow-Script zu testen, als es wirklich laufen zu lassen. So kann man nach einem Tippfehler am Abend zu einer Mailflut mit gefailten Runs am Morgen aufwachen. Das sollte einen dennoch nicht davon abhalten einen lokalen Testlauf durchzuführen.

Kein Datenupdate

Seit Ende Dezember wurden die Parkdaten nicht mehr aktualisiert bzw. bereitgestellt. Das zeigt, dass selbst wenn man einen automatischen Prozess hat, man ihn dennoch weiter überwachen sollte. Ich habe dies erst später festgestellt, wodurch meine Abfragen Ende Dezember immer ins Leere liefen.

Fazit

Trotz der Komplikationen aus dem letzten Kapitel, empfinde ich das Ganze dennoch als einen massiven Erfolg. Während der letzten Monate habe ich mich immer wieder mit dem Thema befasst und die oben beschriebenen Tricks und Kniffe erlernt, die mir auch in Zukunft helfen werden, andere Probleme zu lösen. Ich hoffe, dass auch ihr ein paar wertvolle Hinweise mitnehmen und somit aus meinen Fehlern lernen könnt.

Da ich nun ein gutes halbes Jahr an Daten gesammelt haben, kann ich mich mit der Auswertung befassen. Das wird dann aber erst Gegenstand eines weiteren Blogbeitrages. Jakob Gepp

Nearly one year ago, I analyzed how we use emojis in our Slack messages. Since then, STATWORX grew, and we are a lot more people now! So, I just wanted to check if something changed.

Last time, I did not show our custom emojis, since they are, of course, not available in the fonts I used. This time, I will incorporate them with geom_image(). It is part of the ggimage package from Guangchuang Yu, which you can find here on his Github. With geom_image() you can include images like .png files to your ggplot.

What changed since last year?

Let’s first have a look at the amount of emojis we are using. In the plot below, you can see that since my last analysis in October 2018 (red line) the amount of emojis is rising. Not as much as I thought it would, but compared to the previous period, we now have more days with a usage of over 100 emojis per day!

Like last time, our top emoji is ????, followed by ???? and ????. But sneaking in at number ten is one of our custom emojis: party_hat_parrot!

top-10-used-emojis

How to include custom images?

In my previous blogpost, I hid all our custom emojis behind❓since they were not part of the font. It did not occur to me to use their images, even though the package is from the same creator! So, to make up for my ignorance, I grabbed the top 30 custom emojis and downloaded their images from our Slack servers, saved them as .png and made sure they are all roughly the same size.

To use geom_image() I just added the path of the images to my data (the are just an abbreviation for the complete path).

                NAME COUNT REACTION IMAGE
1:          alnatura    25       63 .../custom/alnatura.png
2:              blog    19       20 .../custom/blog.png
3:           dataiku    15       22 .../custom/dataiku.png
4: dealwithit_parrot     3      100 .../custom/dealwithit_parrot.png
5:      deananddavid    31       18 .../custom/deananddavid.png

This would have been enough to just add the images now, but since I wanted the NAME attribute as a label, I included geom_text_repel from the ggrepel library. This makes handling of non-overlapping labels much simpler!

ggplot(custom_dt, aes( x = REACTION, y = COUNT, label = NAME)) +
  geom_image(aes(image = IMAGE), size = 0.04) +
  geom_text_repel(point.padding = 0.9, segment.alpha = 0) +
  xlab("as reaction") +
  ylab("within message") +
  theme_minimal()

Usually, if a label is „too far“ away from the marker, geom_text_repel includes a line to indicate where the labels belong. Since these lines would overlap the images, I used segment.alpha = 0 to make them invisible. With point.padding = 0.9 I gave the labels a bit more space, so it looks nicer. Depending on the size of the plot, this needs to be adjusted. In the plot, one can see our usage of emojis within a message (y-axis) and as a reaction (x-axis).

To combine the emoji font and custom emojis, I used the following data and code — really… why did I not do this last time? ???? Since the UNICODE is NA when I want to use the IMAGE, there is no „double plotting“.

                     EMOJI REACTION COUNT  SUM PLACE    UNICODE   IMAGE
 1:                    :+1:     1090     0 1090     1 U0001f44d
 2:                   :joy:      609   152  761     2 U0001f602
 3:                 🙂       91   496  587     3 U0001f604
 4:                    :-1:      434     9  443     4 U0001f44e
 5:                  :tada:      346    38  384     5 U0001f389
 6:                  :fire:      274    17  291     6 U0001f525
 7: :slightly_smiling_face:        1   250  251     7 U0001f642
 8:                  😉       27   191  218     8 U0001f609
 9:                  :clap:      201    13  214     9 U0001f44f
10:      :party_hat_parrot:      192     9  201    10       <NA>  .../custom/party_hat_parrot.png
quartz()
ggplot(plotdata2, aes(x = PLACE, y = SUM, label = UNICODE)) +
  geom_bar(stat = "identity", fill = "steelblue") +
  geom_text(family="EmojiOne") +
  xlab("Most popular emojis") +
  ylab("Number of usage") +
  scale_fill_brewer(palette = "Paired") +
  geom_image(aes(image = IMAGE), size = 0.04) +
  theme_minimal()
ps = grid.export(paste0(main_path, "plots/top-10-used-emojis.svg"), addClass=T)
dev.off()

The meaning behind emojis

Now we know what our top emojis are. But what is the rest of the world doing? Thanks to Emojimore for providing me with this overview! On their site, you can find meanings for a lot more emojis.

Behind each of our custom emojis is a story as well. For example, all the food emojis are helping us every day to decide where to eat and provide information on what everyone is planning for lunch! And if you do not agree with the decision, just react with sadphan to let the others know about your feelings. If you want to know the whole stories behind all custom emojis or even help create new ones, then maybe you should join our team — check out our available job offers here!

 

Jakob Gepp

Jakob Gepp

Wenn man mit Matrizen arbeitet, die viele Nullen enthalten, dann sind schwachbesetzte (engl. sparse) Matrizen das richtige. Hierbei wird der benötigte Speicherplatz der Matrix reduziert, in dem der Inhalt der Matrix effizienter verwaltet wird. Es gibt verschiedene Methoden Matrizen zu komprimieren – zum Beispiel in dem nur die Tupel aus Zeile, Spalte und Wert genutzt werden. Die Matrix

A= \left[ {\begin{array}{cccc}  1 & 0 & 0 & 1 \\  0 & 0 & 2 & 0 \\  4 & 0 & 0 & 0 \\  0 & 3 & 0 & 0  \end{array} } \right]

reduziert sich hierbei zu

A_{sparse} =\begin{cases}  1, 1, 1  \\  1, 4, 1  \\  2, 3, 2  \\  3, 1, 4  \\  4, 2, 3 \end{cases}

Durch diese Umformung müssen nicht alle Werte gespeichert werden (Nullen fallen weg), wodurch weniger Platz benötigt wird.

Für Berechnungen mit sparsen Matrizen gibt es in R das package Matrix. Die Berechnungen auf diesen Matrizen sind deutlich effektiver im Speicherverbrauch als die normale base Verwendung. Sollte man nun immer sparse Matrizen verwenden?

Problem

Letztens hatte ich ein Problem mit der Laufzeit meines R Codes und konnte durch Debugging eine Zeile als Wurzel allen Übels identifizieren.

J[, c(2,3] <- J[, c(2,3)] -  const  * B 

Hierbei waren J_{n,k} und J_{n,2} als sparse Matrizen definiert und const ein numerischer Faktor. Das Ersetzen der beiden Spalten dauerte nun sehr lange und es stellte sich raus, dass J und B voll besetzt waren – also doch nicht sparse!
Mir kamen folgende Fragen auf: Bei welchen Operationen lohnen sich sparse Matrizen? Was passiert, wenn eine Matrix nicht schwachbesetzt ist, aber sie dennoch so definiert wird?

Simulation

Um diese Fragen zu beantworten, Für diese Problemstellung habe ich eine kleine Simulation durchgeführt und mir neben dem Speicherbedarf auch die Berechnugnszeiten folgender Operationen angeschaut:

t(X)                           # Transponieren
X %*% t(X)                     # Kreuzprodukt 
X + X                          # Addition 
X * X                          # Matrixmultiplikation 
X[, c(2,3)] <- X[, c(3,2)]     # Spalten vertauschen 

Weitere Einstellungen waren die Spaltenanzahl n der quadratischen Matrix X sowie die Dichte der Nullen innerhalb der Matrix.
Den genauen Code habe ich auf unserem git hinterlegt.

Auswertung

Wie erwartet ist die Reduktion des Speicherplatzes bei kleiner Spaltenanzahl n davon abhängig, wie viele Nullen in der Matrix vorhanden sind. Je höher die Dichte der Nullen, desto geringer der Speicherbedarf. Unter einer Dichte von ca. 50% lohnt es sich nicht mehr sparse Matrizen zu verwenden, um Speicherplatz zu sparen.

Abbildunug zum Speicherbedarf

Egal wie hoch der Anteil der Nullen innerhalb der Matrix ist, bei normalen Matrizen dauern die Matrixoperationen immer gleich lang. Dies erkennt man in der Abbildung daran, dass die durchgezogenen Linien übereinander verlaufen. Bei schwachbesetzten Matrizen brauchen die Berechnungen hingegen deutlich langsamer, wenn es sich um zellenbasierte Operationen handelt. Hierunter fällt auch das Ersetzten ganzer Spalten! Dies ist verständlich, wenn man sich nochmal verdeutlicht, wie die schwachbesetzten Matrizen gespeichert werden.

Abbildung zum Zeitverbrauch

Fazit

Eine Matrix für alle Fälle gibt es nicht. Die Verwendung hängt davon ab, was optimiert werden soll (Rechenzeit oder Speichergröße). Aber auch je nachdem welche Operationen durchgeführt werden müssen, können sich schwachbesetzte Matrizen lohnen oder einem die Laufzeit verlängern! Jakob Gepp

Einer der am häufigsten verwendeten statistischen Tests ist der t-Test. Er kann unter anderem dazu genutzt werden, zu prüfen, ob der Mittelwert einer Zufallsvariable einem bestimmten Wert entspricht. Auch kann er für den Vergleich zweier Mittelwerte herangezogen werden. Wie bei jedem anderen statistischen Test auch, müssen gewisse Voraussetzungen erfüllt sein, damit der t-Test sicher eingesetzt werden kann:

  • Normalverteilung der Zufallsvariablen
  • Unabhängigkeit der Beobachtungen
  • Varianzhomogenität (im Zwei-Gruppenfall)

Wenn diese Voraussetzungen nicht erfüllt sind, ist dennoch nicht aller Tage Abend! Die Robustheit des t-Testes lässt es zu, dass bei größeren Stichprobenumfängen die Normalverteilung weniger wichtig wird(1) . Bei kleinen Stichproben gibt es nicht-parametrische Alternativen, wie den Wilcoxon-Rangsummen-Test. Eine Beschreibung zu diesem Test gibt es hier.

Formeln für unterschiedliche Fälle

Wo genau spielt die Unabhängigkeit eine Rolle? Hierfür ist es wichtig zwischen verschiedenen Fällen zu unterscheiden. Hat man nur eine Variable, so bezieht sich die Unabhängigkeit auf die einzelnen Beobachtungen untereinander. Wenn zwei Zufallsvariablen verglichen werden, dann können folgende Fälle vorliegen, die zu verschiedenen Berechnungen der Teststatistik führen:

Stichprobe
Gepaart / verbunden Ungepaart / unabhängig
Varianzen bekannt  frac{(bar{X}-bar{Y}) - mu_{0} }{ sqrt{ frac{ sigma_{x}^{2}+sigma_{y}^{2} - 2sigma_{xy} }{ n } }} frac{ (bar{X}-bar{Y}) - mu_{0} }{ sqrt{ frac{ sigma_{x}^{2} }{ n_{x}} + frac{ sigma_{y}^{2} }{ n_{y} } } }
unbekannt gleich frac{ (bar{X}-bar{Y}) - mu_{0} }{ frac{ S_{D} }{sqrt{ n } }} frac{ (bar{X}-bar{Y}) - mu_{0} }{ S cdot sqrt{ frac{ n_{x} + n_{y} }{ n_{x} cdot n_{y} } } }
ungleich frac{ (bar{X}-bar{Y}) - mu_{0} }{ sqrt{ frac{ S_{x}^{2} }{ n_{x}} + frac{ S_{y}^{2} }{ n_{y} } } }

Hierbei stehen

  • sigma für die Varianz
  • S für die geschätzen Varianzen
  • S = frac{ (n_{x}-1) cdot S_{x}^{2}+ (n_{y}-1) cdot S_{y}^{2} }{ n_{x} + n_{y} -2 }
  • S_{D}^{2} = frac{ 1 }{ n-1 } sum_{ i = 1 }^n (D_{i}-bar{D})

Zum Überprüfen der Voraussetzungen sollte neben weiteren statistischen Tests auch der inhaltliche Aspekt der Variablen genutzt werden. So kann die Frage, ob es sich um eine gepaarte Stichprobe handelt, schon durch die Betrachtung der Untersuchungsmethodik geklärt werden. Wurde zum Beispiel eine Gruppe vor und nach einer Behandlung untersucht, so handelt es sich um eine gepaarte (auch verbunden genannte) Stichprobe.

Wenn die Voraussetzungen geprüft und der t-Test durchgeführt wurden, steht die eigentliche Interpretation der Ergebnisse an. Was genau bedeuten die Zahlen? Welche Schlüsse können gezogen werden? Ist das Ergebnis signifikant? Mit einem kleinen Beispiel sollen diese Fragen geklärt werden.

Beispiel für SPSS Ausgaben

Wir haben insgesamt 400 Beobachtungen simuliert – sagen wir es handelt sich um die Schlafdauer in Stunden. Wir nehmen folgende drei Szenarien an:

  1. alle Daten stammen von einer Gruppe (Test bei einer Stichprobe)
  2. die Daten stammen von einer Gruppe in einem Vorher-Nachher-Vergleich (Test bei gepaarten Stichproben)
  3. die Daten stammen von zwei verschiedenen Gruppen (Test bei unabhängigen Stichproben)

In SPSS gibt es für den t-Test genau diese drei Fälle: bei einer Stichprobe, bei unabhängigen und bei verbundenen Stichproben. Der Output unterscheidet sich leicht, wie in den Abbildungen zu erkennen ist. Dennoch sind die wesentlichen Interpretationen dieselben.

SPSS Auswertung Einstichprobentest

SPSS Ausgabe beim Einstichprobentest

SPSS Auswertung abhängiger Stichproben

SPSS Ausgabe beim Zweistichprobentest bei abhängigen Stichproben

SPSS Auswertung unabhängiger Stichproben

SPSS Ausgabe beim Zweistichprobentest bei unabhängigen Stichproben

Folgende Werte sind immer gegeben:

  • der Wert der Teststatistik „T“
  • die Anzahl der Freiheitsgrade „df“ (degree of freedom)
  • der p-Wert „Sig. (2-seitig)“
  • die „Untere“ und „Obere“ Grenze des Konfidenzintervalls
  • der „Mittelwert“ bzw. die „Mittlere Differenz“

Diese Kennzahlen hängen stark miteinander zusammen. So wird der p-Wert mittels dem Wert der Teststatistik und dem Freiheitsgrad bestimmt. Die Grenzen des Konfidenzintervalls sind eine andere Darstellung dafür, ob ein Test signifikant ist oder nicht. Sie enthalten die selbe Aussagekraft wie der p-Wert. Der „Mittelwert“ bzw. die „Mittlere Differenz“ gibt die Abweichung der Daten entweder zum Mittelwert oder unter den Gruppen an. Er hilft, die Aussage des t-Tests in Relation zur Fragestellung zu bringen: Welche Gruppe ist größer? In welche Richtung zeigt der Effekt?

Die Ausgabe des t-Test bei unabhängigen Stichproben enthält zudem noch den Levene-Test. Dieser dient zum Überprüfen der Varianzgleichheit (H_{0}: sigma_{1} = sigma_{2}). Je nachdem ob hier eine Signifikanz vorliegt, muss die entsprechende Zeile der Tabelle für die Auswertung genutzt werden. Die Werte der beiden Zeilen können sich unterscheiden, was auf die zuvor erwähnten, unterschiedlichen Formeln zurückzuführen ist.

Auswertung

Was heißt das nun für unser Beispiel? Wie zu erkennen ist, sind die Kennzahlen bei den drei Tests verschieden.

Szenario – Test bei einer Stichprobe
Es gibt hier keinen signifikanten Hinweis darauf, dass der Mittelwert nicht 4,2 in den gesamten Daten ist.

Szenario – Test bei gepaarten Stichproben
Der Unterschied zwischen Vorher und Nachher ist ganz knapp nicht signifikant für alpha = 0,05

Szenario – Test bei unabhängigen Stichproben
Es zeigt sich, dass von Varianzhomogenität ausgegangen werden kann und es einen für alpha = 0,05 signifikanten Unterschied in den Gruppen gibt.

Zusammenfassung

Die verschiedenen t-Tests können durch Unterschiede in der Berechnung auch unterschiedliche Ergebnisse liefern. Die kann im Extremfall – wie in unserem Beispiel – auch die Signifikanz beeinflussen. Es ist also wichtig sich im Vorfeld darüber klar zu werden, welche Struktur die Daten haben, die man untersucht.

Referenzen

  1. Eid, Gollwitzer, Schmitt (2015) Statistik und Forschungsmethoden, Kapitel: 12.1, S. 369ff

Jakob Gepp

Ein bekanntes Beispiel aus der Statistik: Je mehr Leute in Kentucky heiraten, desto mehr Menschen ertrinken nachdem sie aus einem Fischerboot gefallen sind. Mit einem Korrelationskoeffizienten von r = 0.952 ist dieser Zusammenhang, statistisch gesehen, fast perfekt. Aber sollte man deswegen in Kentucky lieber auf das Heiraten verzichten? Oder ist etwa der Pro-Kopf-Verbrauch von Käse verantwortlich für ein unglückliches Ableben durch Verheddern im Bettlaken? Immerhin kann auch hier eine starke Korrelation beobachtet werden (r = 0.947).

Für beide Fälle lautet die Antwort wohl eher „nein“. Stattdessen sollen diese Beispiele klarmachen, dass eine Korrelation noch lange keine Kausalität nachweist. Wozu dienen also Korrelationsanalysen und worauf muss man bei deren Interpretation achten?

Korrelative Zusammenhänge

Eine Analyse der Korrelation zweier Variablen ist immer dann interessant, wenn wir wissen wollen, ob ein statistischer Zusammenhang zwischen diesen Variablen besteht und in welche Richtung dieser verläuft. Dabei unterscheiden wir vier grundlegende Szenarien, die das folgende Beispiel verdeutlichen soll: „Gibt es einen Zusammenhang zwischen der Anzahl an wöchentlichen Arbeitsstunden und der Häufigkeit an Restaurantbesuchen einer Person?“

  • Kein Zusammenhang: Durch Kenntnis der wöchentlichen Arbeitsstunden kann keinerlei Aussage über die Häufigkeit an Restaurantbesuchen gemacht werden.
  • Positiver Zusammenhang: Je mehr eine Person pro Woche arbeitet, desto häufiger besucht sie ein Restaurant.
  • Negativer Zusammenhang: Je mehr eine Person pro Woche arbeitet, desto seltener besucht sie ein Restaurant.
  • Nichtlinearer Zusammenhang: Sowohl eine unter- als auch überdurchschnittliche Anzahl an wöchentlichen Arbeitsstunden erhöht die Häufigkeit von Restaurantbesuchen.

Ob der beobachtete Zusammenhang auch eine kausale Verknüpfung hat, welche Variable Ursache und welche Wirkung ist – diese Fragen bleiben von der Korrelationsanalyse unbeantwortet. Nehmen wir an, wir würden für unser Beispiel einen positiven Zusammenhang beobachten. Dann könnte eine Erklärung lauten, dass Personen die länger arbeiten, weniger Zeit zum Kochen haben und deshalb häufiger auf Restaurants ausweichen. Alternativ denkbar wäre auch, dass Personen die gerne essen gehen mehr arbeiten müssen, um sich ihre häufigen Restaurantbesuche leisten zu können. Auch ein rein zufälliges Entstehen der Korrelation ist nicht auszuschließen, wie die zwei Eingangsbeispiele klarmachen sollten.

Keine Kausalität in Korrelation

Wir wissen also nicht, ob ein kausaler, ursächlicher Zusammenhang vorliegt, was genau Ursache und was Wirkung ist. Trotzdem kann es natürlich wünschenswert sein durch (gründlich recherchierte) inhaltliche Interpretation aus einem korrelativen Zusammenhang eine Kausalität abzuleiten. Ganz wichtig ist es aber sich bewusst zu machen, dass diese Interpretationen, so schlüssig sie erscheinen mögen, nie von der Korrelation statistisch belegt sind.

Kausalität nachweisen

Tatsächlich lässt sich ein kausaler Zusammenhang nie mit statistischen Methoden vollständig nachweisen (wobei es hier in der Statistik neue Stoßrichtungen gibt, z.B. zum Thema Causal Inference). Die beste Approximation erhalten wir durch ein kontrolliertes Experiment, d.h. durch Manipulation der unabhängigen Variable X (angenommen als Ursache, z.B. wöchentliche Arbeitsstunden) bei gleichzeitiger Beobachtung der abhängigen Variable Y (angenommen als Wirkung, z.B. Anzahl Restaurantbesuche). Verändert sich nun Y infolge der Manipulation von X, kann, zumindest statistisch von Zusammenhang der beiden Faktoren ausgegangen werden.

Korrelationskoeffizienten

Zur Berechnung von Korrelationen stehen dem Wissenschaftler verschiedene Korrelationskoeffizienten zur Verfügung. Diese werden, je nach Skalenniveau der Daten und vermutetem Zusammenhang, ausgewählt. Die beiden wichtigsten Korrelationskoeffizienten sind der Pearson-Korrelationskoeffizient sowie der Spearman-Korrelationskoeffizient. Ersterer wird verwendet, wenn beide zu korrelierenden Variablen metrisch bzw. intervallskaliert und normalverteilt sind. Die Spearman Korrelation hingegen wird basierend auf Rangdaten berechnet und ist auch für ordinale und nicht-normalverteilte Daten geeignet. Beide Koeffizienten sind im Intervall zwischen r = -1 und r = 1 definiert, wobei r = -1 einen perfekten negativen und r = 1 einen perfekten positiven Zusammenhang beschreiben.

Praktischer Einsatz von Korrelationen

In der statistischen Praxis werden Korrelationen häufig im Rahmen der explorativen Datenanalyse verwendet, d.h. als erste Indikation für etwaige statistische Effekte, die mit komplexeren Methoden, wie z.B. der Regressionsanalyse, weiter untersucht werden. Dies wird auch vor dem Hintergrund klarer, dass bei einfachen Korrelationsanalysen keine weiteren Variablen zur Kontrolle der Wirkung verwendet werden können. Man geht also davon aus, dass lediglich eine Wirkung von X auf Y vorliegt und keine anderen Faktoren Y beeinflussen. Dies ist für die meisten Experimente eine extrem unplausible Annahme.

Zusammenfassung

Wichtig ist zu verstehen, dass mit statistischen Korrelationen keine Aussagen über kausale Wirkzusammenhänge getroffen werden können. Alle statistischen Modelle sind lediglich einfache Abstraktionen der Wirklichkeit und werden in den meisten Fällen nie den tatsächlichen Kausalzusammenhang zwischen Variablen abbilden können. Aber, um es mit den Worten des berühmten Statistikers George Box zu sagen: „All models are wrong…but some of them are useful.“. Falls du Unterstützung bei der Auswahl oder Berechnung von Korrelationen benötigst, hilft dir unser Statistik Team gerne weiter.

Causal Inference: http://egap.org/methods-guides/10-things-you-need-know-about-causal-inference
All models are wrong: https://en.wikipedia.org/wiki/All_models_are_wrong Jakob Gepp

Ein bekanntes Beispiel aus der Statistik: Je mehr Leute in Kentucky heiraten, desto mehr Menschen ertrinken nachdem sie aus einem Fischerboot gefallen sind. Mit einem Korrelationskoeffizienten von r = 0.952 ist dieser Zusammenhang, statistisch gesehen, fast perfekt. Aber sollte man deswegen in Kentucky lieber auf das Heiraten verzichten? Oder ist etwa der Pro-Kopf-Verbrauch von Käse verantwortlich für ein unglückliches Ableben durch Verheddern im Bettlaken? Immerhin kann auch hier eine starke Korrelation beobachtet werden (r = 0.947).

Für beide Fälle lautet die Antwort wohl eher „nein“. Stattdessen sollen diese Beispiele klarmachen, dass eine Korrelation noch lange keine Kausalität nachweist. Wozu dienen also Korrelationsanalysen und worauf muss man bei deren Interpretation achten?

Korrelative Zusammenhänge

Eine Analyse der Korrelation zweier Variablen ist immer dann interessant, wenn wir wissen wollen, ob ein statistischer Zusammenhang zwischen diesen Variablen besteht und in welche Richtung dieser verläuft. Dabei unterscheiden wir vier grundlegende Szenarien, die das folgende Beispiel verdeutlichen soll: „Gibt es einen Zusammenhang zwischen der Anzahl an wöchentlichen Arbeitsstunden und der Häufigkeit an Restaurantbesuchen einer Person?“

Ob der beobachtete Zusammenhang auch eine kausale Verknüpfung hat, welche Variable Ursache und welche Wirkung ist – diese Fragen bleiben von der Korrelationsanalyse unbeantwortet. Nehmen wir an, wir würden für unser Beispiel einen positiven Zusammenhang beobachten. Dann könnte eine Erklärung lauten, dass Personen die länger arbeiten, weniger Zeit zum Kochen haben und deshalb häufiger auf Restaurants ausweichen. Alternativ denkbar wäre auch, dass Personen die gerne essen gehen mehr arbeiten müssen, um sich ihre häufigen Restaurantbesuche leisten zu können. Auch ein rein zufälliges Entstehen der Korrelation ist nicht auszuschließen, wie die zwei Eingangsbeispiele klarmachen sollten.

Keine Kausalität in Korrelation

Wir wissen also nicht, ob ein kausaler, ursächlicher Zusammenhang vorliegt, was genau Ursache und was Wirkung ist. Trotzdem kann es natürlich wünschenswert sein durch (gründlich recherchierte) inhaltliche Interpretation aus einem korrelativen Zusammenhang eine Kausalität abzuleiten. Ganz wichtig ist es aber sich bewusst zu machen, dass diese Interpretationen, so schlüssig sie erscheinen mögen, nie von der Korrelation statistisch belegt sind.

Kausalität nachweisen

Tatsächlich lässt sich ein kausaler Zusammenhang nie mit statistischen Methoden vollständig nachweisen (wobei es hier in der Statistik neue Stoßrichtungen gibt, z.B. zum Thema Causal Inference). Die beste Approximation erhalten wir durch ein kontrolliertes Experiment, d.h. durch Manipulation der unabhängigen Variable X (angenommen als Ursache, z.B. wöchentliche Arbeitsstunden) bei gleichzeitiger Beobachtung der abhängigen Variable Y (angenommen als Wirkung, z.B. Anzahl Restaurantbesuche). Verändert sich nun Y infolge der Manipulation von X, kann, zumindest statistisch von Zusammenhang der beiden Faktoren ausgegangen werden.

Korrelationskoeffizienten

Zur Berechnung von Korrelationen stehen dem Wissenschaftler verschiedene Korrelationskoeffizienten zur Verfügung. Diese werden, je nach Skalenniveau der Daten und vermutetem Zusammenhang, ausgewählt. Die beiden wichtigsten Korrelationskoeffizienten sind der Pearson-Korrelationskoeffizient sowie der Spearman-Korrelationskoeffizient. Ersterer wird verwendet, wenn beide zu korrelierenden Variablen metrisch bzw. intervallskaliert und normalverteilt sind. Die Spearman Korrelation hingegen wird basierend auf Rangdaten berechnet und ist auch für ordinale und nicht-normalverteilte Daten geeignet. Beide Koeffizienten sind im Intervall zwischen r = -1 und r = 1 definiert, wobei r = -1 einen perfekten negativen und r = 1 einen perfekten positiven Zusammenhang beschreiben.

Praktischer Einsatz von Korrelationen

In der statistischen Praxis werden Korrelationen häufig im Rahmen der explorativen Datenanalyse verwendet, d.h. als erste Indikation für etwaige statistische Effekte, die mit komplexeren Methoden, wie z.B. der Regressionsanalyse, weiter untersucht werden. Dies wird auch vor dem Hintergrund klarer, dass bei einfachen Korrelationsanalysen keine weiteren Variablen zur Kontrolle der Wirkung verwendet werden können. Man geht also davon aus, dass lediglich eine Wirkung von X auf Y vorliegt und keine anderen Faktoren Y beeinflussen. Dies ist für die meisten Experimente eine extrem unplausible Annahme.

Zusammenfassung

Wichtig ist zu verstehen, dass mit statistischen Korrelationen keine Aussagen über kausale Wirkzusammenhänge getroffen werden können. Alle statistischen Modelle sind lediglich einfache Abstraktionen der Wirklichkeit und werden in den meisten Fällen nie den tatsächlichen Kausalzusammenhang zwischen Variablen abbilden können. Aber, um es mit den Worten des berühmten Statistikers George Box zu sagen: „All models are wrong…but some of them are useful.“. Falls du Unterstützung bei der Auswahl oder Berechnung von Korrelationen benötigst, hilft dir unser Statistik Team gerne weiter.

Causal Inference: http://egap.org/methods-guides/10-things-you-need-know-about-causal-inference
All models are wrong: https://en.wikipedia.org/wiki/All_models_are_wrong Jakob Gepp