67  Data preprocessing

Letzte Änderung am 23. March 2024 um 21:57:33

“I confess that, in 1901, I said to my brother Orville that men would not fly for 50 years. Two years later, we were making flights. This demonstration of my inability as a prophet gave me such a shock that I have ever since refrained from all prediction.” — Wilbur Wright

Die Vorverarbeitung von Daten (eng. preprocessing) für die Klassifikation grundlegend. Wir können nicht auf unseren Daten so wir wie die Daten erhoben haben eine Klassifikation rechnen. Dafür sind die Algorithmen der Klassifikation weder ausgelegt noch gedacht. Zum anderen wollen wir ja gar keine Aussagen über mögliche Effekte von den Einflussvariablen auf das Outcome treffen. Uns ist vollkommen egal, ob eine Variable signifikant ist. Wir wollen nur wissen, ob eine Variable wichtig für die Vorhersage von unserem Label \(y\) ist.

Du findest auch noch im Appendix von Tidy Modeling with R die Recommended Preprocessing Schritte für viele Algorithmen.

Du findest auf der Referenzseite von recipes eine große Auswahl an Vorverarbeitungsschritten. Ich kann dir hier nur eine Auswahl präsentieren und konzentriere mich auf die häufigste genutzen Algorithmen. Du solltest aber für deinen Anwendungsfall auf jeden Fall nochmal selber schauen, ob du was passenderes findest.

Einige Vorverarbeitungsschritte kannst du auch in den vorherigen Kapiteln nachlesen. Im Kapitel zur Transformation von Daten oder zur Imputation von fehlenden Werten findest du noch tiefer greifende Informationen zu den Themen. In diesem Kapitel zeige ich nur, wie du die Verfahren anwendest und gehe nochmal eher oberflächlich auf mögliche Probleme ein.

Preprocessing und Tuning

Achtung! Du kannst das Preprocessing nicht mit dem Tuning von Algorithmen verbinden. Das heißt, du optimierst deinen Algorithmus immer auf einen Set von pre-prozessierten Daten. Wenn die Daten schlecht sind, dann wird das Tuning auch nicht mehr viel helfen. Deshalb muss man häufig drüber nachdenken, welche Variablen sollen mit in die Analyse und was soll mit den Daten gemacht werden.

67.1 Genutzte R Pakete

Wir wollen folgende R Pakete in diesem Kapitel nutzen.

pacman::p_load(tidyverse, tidymodels, magrittr, 
               janitor,
               conflicted)

An der Seite des Kapitels findest du den Link Quellcode anzeigen, über den du Zugang zum gesamten R-Code dieses Kapitels erhältst.

67.2 Daten

In dieser Einführung nehmen wir die infizierten Ferkel als Beispiel um einmal die verschiedenen Verfahren zu demonstrieren. Ich füge hier noch die ID mit ein, die nichts anderes ist, als die Zeilennummer. Dann habe ich noch die ID an den Anfang gestellt. Wir wählen auch nur ein kleines Subset aus den Daten aus, da wir in diesem Kapitel nur Funktion demonstrieren und nicht die Ergebnisse interpretieren.

pig_tbl <- read_excel("data/infected_pigs.xlsx") |> 
  mutate(pig_id = 1:n()) |> 
  select(pig_id, infected, age, crp, sex, frailty) |> 
  select(pig_id, infected, everything())  

In Tabelle 69.1 siehst du nochmal einen Ausschnitt aus den Daten. Wir haben noch die ID mit eingefügt, damit wir einzelne Beobachtungen nachvollziehen können.

Tabelle 67.1— Auszug aus dem Daten zu den kranken Ferkeln.
pig_id infected age crp sex frailty
1 1 61 22.38 male robust
2 1 53 18.64 male robust
3 0 66 18.76 female robust
4 1 59 19.37 female robust
5 1 63 21.57 male robust
6 1 55 21.45 male robust
407 1 54 21.5 female pre-frail
408 0 56 20.8 male frail
409 1 57 21.95 male pre-frail
410 1 61 23.1 male robust
411 0 59 20.23 female robust
412 1 63 19.89 female robust

Gehen wir jetzt mal die Preprocessing Schritte, die wir für das maschinelle Lernen später brauchen einmal durch. Am Ende des Kapitels schauen wir uns dann die Anwendung nochmal im Ganzen auf den Gummibärchendaten einmal an.

67.3 Das Rezept mit recipe()

In dem Einführungskapitel zur Klassifikation haben wir uns ja mit dem Rezept und dem Workflow schon mal beschäftigt. Hier möchte ich dann nochmal etwas mehr auf das Rezept eingehen und zeigen, wie das Rezept für Daten dann mit den Daten zusammenkommt. Wir bauen uns wie immer mit der Funktion recipe() das Datenrezept in R zusammen. Ich empfehle grundsätzlich vorab einen select() Schritt durchzuführen und nur die Variablen in den Daten zu behalten, die wir wirklich brauchen. Dann können wir auch mit dem . einfach alle Spalten ohne das Outcome als Prädiktoren definieren.

pig_rec <- recipe(infected ~ ., data = pig_tbl) |> 
  update_role(pig_id, new_role = "ID")

pig_rec |> summary()
# A tibble: 6 × 4
  variable type      role      source  
  <chr>    <list>    <chr>     <chr>   
1 pig_id   <chr [2]> ID        original
2 age      <chr [2]> predictor original
3 crp      <chr [2]> predictor original
4 sex      <chr [3]> predictor original
5 frailty  <chr [3]> predictor original
6 infected <chr [2]> outcome   original

Nachdem wir dann unser Rezept definiert haben, können wir auch noch Rollen vergeben. Die Rollen sind nützlich, wenn wir später auf bestimmten Variablen etwas rechnen wollen oder eben nicht. Wir können die Rollen selber definieren und diese Rollen dann auch über die Funktion has_role() ein- oder ausschließen. Neben dieser Möglichkeit gezielt Variablen nach der Rolle anzusprechen, können wir auch alle Prädiktoren oder alle Outcomes auswählen.

Wir haben Funktionen, die die Rolle der Variablen festlegen:

  • all_predictors() wendet den Schritt nur auf die Prädiktorvariablen an, daher auf die Features.
  • all_outcomes() wendet den Schritt nur auf die Outcome-Variable(n) an, daher auf die Label.

Un wir haben Funktionen, die den Typ der Variablen angeben:

  • all_nominal() wendet den Schritt auf alle Variablen an, die nominal (kategorisch) sind.
  • all_numeric() wendet den Schritt auf alle Variablen an, die numerisch sind.

Und natürlich deren Kombination wie all_nominal_predictors() oder all_numeric_predictors(), die dann eben auf die Prädiktoren, die nominal also Faktoren oder Gruppen repräsentieren oder eben numerischen Variablen, angewendet werden. Du wirst die Anwendung gleich später in den Rezeptschritten sehen, da macht die Sache dann sehr viel mehr Sinn.

Nun ist es aber auch so, dass es bei dem Rezept auf die Reihenfolge der einzelnen Schritte ankommt. Die Reihenfolge der Zutaten und damit der Rezeptschritte sind ja auch beim Kuchenbacken sehr wichtig! Da das Rezept wirklich in der Reihenfolge durchgeführt wird, wie du die einzelnen Schritte angibst, empfiehlt sich folgende Reihenfolge. Du musst natürlich nicht jeden dieser Schritte auch immer durchführen.

Bitte die Hinweise zur Ordnung der Schritte eines Rezeptes beachten: Ordering of steps

  1. Entfernen von Beobachtungen mit einem fehlenden Eintrag für das Label.
  2. Imputation von fehlenden Werten in den Daten.
  3. Individuelle Transformationen auf einzelnen Spalten.
  4. Umwandeln von einzelnen numerischen Variablen in eine diskrete Variable.
  5. Erstellung der Dummyvariablen für jede diskrete Variable.
  6. Eventuell Berücksichtigung der Interaktion zwischen Variablen.
  7. Transformation der numerischen Variablen mit zum Beispiel der Standarisierung oder Normalisierung.
  8. Multivariate Transformationen über alle Spalten hinweg wie zum Beispiel PCA.

Am Ende wollen wir dann natürlich auch die Daten wiederhaben. Das heißt, wir bauen ja das Rezept auf einem Datensatz. Wenn wir dann das fertige Rezept in die Funktion prep() pipen können wir über die Funktion juice() den ursprünglichen jetzt aber transformierten Datensatz wieder erhalten. Wenn wir das Rezept auf einen neuen Datensatz anwenden wollen, dann nutzen wir die Funktion bake(). Mit einem neuen Datensatz meine ich natürlich einen Split in Training- und Testdaten von dem ursprünglichen Datensatz. In dem neuen Datensatz müssen natürlich alle Spaltennamen auch enthalten sein, sonst macht die Sache recht wenig Sinn.

67.4 Fehlende Werte im \(Y\)

Wenn wir mit maschinellen Lernverfahren rechnen, dann dürfen wir im Outcome \(Y\) oder dem Label keine fehlenden Werte vorliegen haben. Das Outcome ist in dem Sinne hielig, dass wir hier keine Werte imputieren. Wir müssen daher alle Zeilen und damit Beobachtungen aus den Daten entfernen in denen ein NA im Outcome vorliegt. Wir können dazu die Funktion drop_na() nutzen. Wir können in der Funktion spezifizieren, dass nur für eine Spalte die NA entfernt werden sollen. In unserem Beispiel für die Ferkeldaten wäre es dann die Spalte infected.

drop_na(infected)

Aktuell haben wir ja keine fehlenden Werte in der Spalte vorliegen, so dass wir die Funktion hier nicht benötigen. In dem Beispiel zu den Gummibärchendaten wollen wir das Geschlecht vorhersagen und hier haben wir dann fehlende Werte im Outcome. Mit der Funktion drop_na(gender) entfernen wir dann alle Beobachtungen aus den Daten mit einem fehlenden Eintrag für das Geschlecht.

67.5 Dummycodierung von \(X\)

Wir werden immer häufiger davon sprechen, dass wir alle kategorialen Daten in Dummies überführen müssen. Das heißt, wir dürfen keine Faktoren mehr in unseren Daten haben. Wir wandeln daher alle Variablen, die ein Faktor sind, in Dummyspalten um. Die Idee von der Dummyspalte ist die gleiche wie bei der multiplen Regression. Da ich aber nicht davon ausgehe, dass du dir alles hier durchgelesen hast, kommt hier die kurze Einführung zur Dummycodierung.

Die Dummycodierung wird nur auf den Features durchgeführt. Dabei werden nur Spalten erschaffen, die \(0/1\), für Level vorhanden oder Level nicht vorhanden, beinhalten. Wir werden also nur alle \(x\) in Dummies umwandeln, die einem Faktor entsprechen. Dafür nutzen wir dann später eine Funktion, hier machen wir das einmal zu Veranschaulichung per Hand. In Tabelle 67.2 haben wir einen kleinen Ausschnitt unser Schweinedaten gegeben. Wir wollen zuerst die Spalte sex in eine Dummycodierung umwandeln.

Tabelle 67.2— Beispieldatensatz für die Dummycodierung. Wir wollen die Spalten sex und frailty als Dummyspalten haben.
infected age sex frailty
1 24 male robust
0 36 male pre-frail
0 21 female frail
1 34 female robust
1 27 male frail

In der Tabelle 67.3 sehen wir das Ergebnis für die Dummycodierung der Spalte sex in die Dummyspalte sex_male. Wir haben in der Dummyspalte nur noch die Information, ob das Ferkel mänlich ist oder nicht. Wenn wir eine Eins in der Spalte finden, dann ist das Ferkel männlich. Wenn wir eine Null vorfinden, dann ist das Ferkel nicht männlich also weiblich. Das Nicht müssen wir uns dann immer merken.

Tabelle 67.3— Ergebnis der Dummycodierung der Spalte sex zu der Spalte sex_male.
infected age sex_male
1 24 1
0 36 1
0 21 0
1 34 0
1 27 1

In der Tabelle 67.4 betrachten wir einen komplexeren Fall. Wenn wir eine Spalte vorliegen haben mit mehr als zwei Leveln, wie zum Beispiel die Spalte frailty, dann erhalten wir zwei Spalten wieder. Die Spalte frailty_robust beschreibt das Vorhandensein des Levels robust und die Spalte frailty_pre-frail das Vorhandensein des Levels pre-frail. Und was ist mit dem Level frail? Das Level wir durch das Nichtvorhandensein von robust und dem Nichtvorhandensein von pre-frail abgebildet. Beinhalten beide Spalten die Null, so ist das Ferkel frail.

Tabelle 67.4— Ergebnis der Dummycodierung für eine Spalte mit mehr als zwei Leveln.
infected age frailty_robust frailty_pre-frail
1 24 1 0
0 36 0 1
0 21 0 0
1 34 1 0
1 27 0 0

Wenn wir einen Faktor mit \(l\) Leveln haben, erhalten wir immer \(l-1\) Spalten nach der Dummycodierung wieder.

Wir nutzen dann die Funktion step_dummy() um eine Dummaycodierung für alle nominalen Prädiktoren spezifiziert durch all_nominal_predictors() durchzuführen. Das tolle ist hier, dass wir durch die Helferfunktionen immer genau sagen können welche Typen von Spalten bearbeitet werden sollen.

pig_dummy_rec <- pig_rec |> 
  step_dummy(all_nominal_predictors()) 

pig_dummy_rec 
── Recipe ──────────────────────────────────────────────────────────────────────
── Inputs 
Number of variables by role
outcome:   1
predictor: 4
ID:        1
── Operations 
• Dummy variables from: all_nominal_predictors()

Wenn wir das Rezept fertig haben, dann können wir uns die Daten einmal anschauen. Durch die Funktion prep() initialisieren wir das Rezept und mit der Funktion juice() teilen wir mit, dass wir das Rezept gleich auf die Trainingsdaten mit denen wir das Rezept gebaut haben, anweden wollen.

pig_dummy_rec |>
  prep() |>
  juice() 
# A tibble: 412 × 7
   pig_id   age   crp infected sex_male frailty_pre.frail frailty_robust
    <int> <dbl> <dbl>    <dbl>    <dbl>             <dbl>          <dbl>
 1      1    61  22.4        1        1                 0              1
 2      2    53  18.6        1        1                 0              1
 3      3    66  18.8        0        0                 0              1
 4      4    59  19.4        1        0                 0              1
 5      5    63  21.6        1        1                 0              1
 6      6    55  21.4        1        1                 0              1
 7      7    49  19.0        1        1                 1              0
 8      8    53  19.0        0        1                 0              1
 9      9    58  21.9        1        0                 0              1
10     10    57  21.0        1        1                 0              1
# ℹ 402 more rows

Die Dummycodierung verwandelt alle nominalen Spalten in mehrere \(0/1\) Spalten um. Das ermöglicht den Algorithmen auch mit nominalen Spalten eine Vorhersage zu machen.

67.6 Zero Variance Spalten

Ein häufiges Problem ist, dass wir manchmal Spalten in unseren Daten haben in denen nur ein Eintrag steht. Das heißt wir haben überall die gleiche Zahl oder eben das gelche Wort stehen. Das tritt häufiger auf, wenn wir uns riesige Datenmengen von extern herunterladen. Manchmal haben wir so viele Spalten, dass wir die Daten gr nicht richtig überblicken. Oder aber, wir haben nach einer Transformation nur noch die gleiche Zahl. Dagegen können wir filtern.

Wir haben die Auswahl zwischen step_zv(), die Funktion entfernt Spalten mit einer Vaianz von Null. Das mag seltener vorkommen, als eine sehr kleine Varianz. Hier hilft die Funktion step_nzv(). Wir können beide Funktionen auf alle Arten von Prädiktoren anwenden, nur eben nicht gleichzeitig.

pig_zero_rec <- pig_rec |> 
  step_zv(all_predictors()) |> 
  step_nzv(all_predictors())

pig_zero_rec
── Recipe ──────────────────────────────────────────────────────────────────────
── Inputs 
Number of variables by role
outcome:   1
predictor: 4
ID:        1
── Operations 
• Zero variance filter on: all_predictors()
• Sparse, unbalanced variable filter on: all_predictors()

Da wir in unseren Daten mit den infizierten Ferkeln jetzt keine Spalten mit einer sehr kleinen Varianz haben, passiert auch nichts, wenn wir die Funktion auf unsere Daten anwenden würden. Demensprechend sparen wir uns an dieser Stelle auch die Datengenerierung.

67.7 Standardisieren \(\mathcal{N}(0,1)\)

In dem Kapitel zu der Transformation von Daten haben wir ja schon von der Standardisierung gelesen und uns mit den gängigen Funktion beschäftigt. Deshalb hier nur kurz die Schritte und Funktionen, die wir mit den Rezepten machen können. Zum einen können wir nur die Daten mit der Funktion step_scale() skalieren, dass heißt auf eine Standardabweichung von 1 bringen. Oder aber zum anderen nutzen wir die Funktion scale_center() um die Daten alle auf einen Mittelwert von 0 zu schieben. Manchmal wollen wir nur den einen Schritt getrennt von dem anderen Schritt durchführen. Beide Schritte können wir dann einfach auf allen numerischen Prädiktoren durchführen.

pig_scale_center_rec <- pig_rec |> 
  step_center(all_numeric_predictors()) |> 
  step_scale(all_numeric_predictors()) 

pig_scale_center_rec 
── Recipe ──────────────────────────────────────────────────────────────────────
── Inputs 
Number of variables by role
outcome:   1
predictor: 4
ID:        1
── Operations 
• Centering for: all_numeric_predictors()
• Scaling for: all_numeric_predictors()

Wenn wir aber auf eine getrennte Durchführung keine Lust haben, gibt es auch die etwas schief benannte Funktion step_normalize(), die beide Schritte kombiniert und uns damit die Daten auf eine Standardnormalverteilung transformiert. Ich persönlich nutze dann meist die zweite Variante, dann hat man alles in einem Schritt zusammen. Das hängt aber sehr vom Anwendungsfall ab und du musst dann schauen, was besser für dich und deine Daten dann passt.

pig_scale_center_rec <- pig_rec |> 
  step_normalize(all_numeric_predictors()) 

pig_scale_center_rec 
── Recipe ──────────────────────────────────────────────────────────────────────
── Inputs 
Number of variables by role
outcome:   1
predictor: 4
ID:        1
── Operations 
• Centering and scaling for: all_numeric_predictors()

Jetzt können wir noch die Daten generieren und sehen, dass wir alle numerischen Spalten in eine Standardnormalverteilung transformiert haben. Wir runden hier nochmal alle numerischen Variablen, damit wir nicht so einen breiten Datensatz erhalten. Das hat jetzt aber eher was mit der Ausgabe hier auf der Webseite zu tun. Wir müssen nicht runden um die Daten dann zu verwenden.

pig_scale_center_rec |>
  prep() |>
  juice() |> 
  mutate(across(where(is.numeric), round, 2))
# A tibble: 412 × 6
   pig_id   age   crp sex    frailty   infected
    <dbl> <dbl> <dbl> <fct>  <fct>        <dbl>
 1      1  0.22  1.62 male   robust           1
 2      2 -1.55 -0.99 male   robust           1
 3      3  1.32 -0.91 female robust           0
 4      4 -0.23 -0.48 female robust           1
 5      5  0.66  1.05 male   robust           1
 6      6 -1.11  0.97 male   robust           1
 7      7 -2.44 -0.76 male   pre-frail        1
 8      8 -1.55 -0.76 male   robust           0
 9      9 -0.45  1.27 female robust           1
10     10 -0.67  0.62 male   robust           1
# ℹ 402 more rows

67.8 Normalisieren \([0; 1]\)

Auch bei der Normalisierung möchte ich wieder auf das Kapitel zum Transformation von Daten verweisen. In dem tidymodels Universum heißt dann das Normalisieren, also die Daten auf eine Spannweite zwischen 0 und 1 bringen, dann eben step_range(). Das ist natürlich dann schön generalisiert. Wir könnten uns auch andere Spannweiten überlegen, aber hier nehmen wir natürlich immer den Klassiker auf eine Spannweite \([0; 1]\). Unsere Daten liegen dann nach der Normalisierung mit der Funktion step_range() zwischen 0 und 1. Wir können die Normalisierung natürlich nur auf numerischen Variablen durchführen.

pig_range_rec <- pig_rec |> 
  step_range(all_numeric_predictors(), min = 0, max = 1) 

pig_range_rec 
── Recipe ──────────────────────────────────────────────────────────────────────
── Inputs 
Number of variables by role
outcome:   1
predictor: 4
ID:        1
── Operations 
• Range scaling to [0,1] for: all_numeric_predictors()

Auch hier können wir dann die Daten generieren und uns einmal anschauen. Im Gegensatz zu der Standardisierung treten jetzt in unseren Spalten keine negativen Werte mehr auf. Wir runden hier ebenfalls nochmal alle numerischen Variablen, damit wir nicht so einen breiten Datensatz erhalten. Das hat jetzt aber eher was mit der Ausgabe hier auf der Webseite zu tun. Wir müssen nicht runden um die Daten dann zu verwenden.

pig_range_rec |>
  prep() |>
  juice() |> 
  mutate(across(where(is.numeric), round, 2))
# A tibble: 412 × 6
   pig_id   age   crp sex    frailty   infected
    <dbl> <dbl> <dbl> <fct>  <fct>        <dbl>
 1      1  0.52  0.82 male   robust           1
 2      2  0.17  0.34 male   robust           1
 3      3  0.74  0.36 female robust           0
 4      4  0.43  0.44 female robust           1
 5      5  0.61  0.72 male   robust           1
 6      6  0.26  0.7  male   robust           1
 7      7  0     0.39 male   pre-frail        1
 8      8  0.17  0.39 male   robust           0
 9      9  0.39  0.76 female robust           1
10     10  0.35  0.64 male   robust           1
# ℹ 402 more rows

67.9 Imputieren von fehlenden Werten

In dem Kapitel zur Imputation von fehlenden Werten haben wir uns mit verschiedenen Methoden zur Imputation von fehlenden Werten beschäftigt. Auch gibt es verschiedene Rezepte um die Imputation durchzuführen. Wir haben also wieder die Qual der Wahl welchen Algorithmus wir nutzen wollen. Da wir wieder zwischen numerischen und nominalen Variablen unterscheiden müssen, haben wir immer zwei Imputationsschritte. Ich mache es mir hier sehr leicht und wähle die mean Imputation für die numerischen Variablen aus und die mode Imputation für die nominalen Variablen. Das sind natürlich die beiden simpelsten Imputation die gehen. Ich würde dir empfehlen nochmal die Alternativen anzuschauen und vorab auf jeden Fall nochmal dir die fehlenden Daten zu visualisieren. Es macht auch hier keinen Sinn nicht vorhandene Spalten mit künstlichen Daten zu füllen.

Mehr Information zu Step Functions - Imputation

Da wir es uns in diesem Schritt sehr einfach machen, nutzen wir die Funktionen step_impute_mean() auf allen numerischen Variablen und die Funktion step_impute_mode() auf alle nominalen Variablen. Es geht wie immer natürlich besser, das heißt auch komplexerer. Hier ist es auch wieder schwierig zu sagen, welche Methode die beste Methode zur Imputation von fehlenden Werten ist. Hier hilft es dann nichts, du musst dir die imputierten Daten anschauen.

pig_imp_rec <- pig_rec |> 
  step_impute_mean(all_numeric_predictors()) |> 
  step_impute_mode(all_nominal_predictors())

pig_imp_rec 
── Recipe ──────────────────────────────────────────────────────────────────────
── Inputs 
Number of variables by role
outcome:   1
predictor: 4
ID:        1
── Operations 
• Mean imputation for: all_numeric_predictors()
• Mode imputation for: all_nominal_predictors()

Dann können wir uns auch schon die Daten generieren. Wir sehen, dass wir keine fehlenden Werte mehr in unseren Daten vorliegen haben. Wie immer können wir uns die gerundeten Daten dann einmal anschauen.

pig_imp_rec  |>
  prep() |>
  juice() |> 
  mutate(across(where(is.numeric), round, 2))
# A tibble: 412 × 6
   pig_id   age   crp sex    frailty   infected
    <dbl> <dbl> <dbl> <fct>  <fct>        <dbl>
 1      1    61  22.4 male   robust           1
 2      2    53  18.6 male   robust           1
 3      3    66  18.8 female robust           0
 4      4    59  19.4 female robust           1
 5      5    63  21.6 male   robust           1
 6      6    55  21.4 male   robust           1
 7      7    49  19.0 male   pre-frail        1
 8      8    53  19.0 male   robust           0
 9      9    58  21.9 female robust           1
10     10    57  21.0 male   robust           1
# ℹ 402 more rows

Die Imputationrezepte bieten sich natürlich auch für die ganz normale Statistik an. Du kannst ja dann mit den imputierten Daten rechnen was du möchtest. Wir nutzen die Daten hier ja nur im Kontext der Klassifikation. Es gingt natürlich auch die Daten für die lineare Regression zu nutzen.

67.10 Kategorisierung

Manchmal wollen wir nicht mit numerischen Variablen arbeiten sondern uns nominale Variablen erschaffen. Das sollten wir eigentlich nicht so häufig tun, denn die numerischen Variablen haben meist mehr Informationen als nominale Variablen. Wir müssen dann ja unsere nominalen Daten dann wieder in Dummies umkodieren. Das sind dann zwei zusätzliche Schritte. Aber wie immer in der Datenanalyse, es gibt Fälle in denen es Sinn macht und wir eben keine numerischen Variablen haben wollen. Dann können wir eben die Funktion step_discretize() nutzen um verschiedene Gruppen oder bins (eng. Dosen) zu bilden. Das R Paket {embed} bietet noch eine Vielzahl an weiteren Funktionen für die Erstellung von kategorialen Variablen.

Es kann natürlich sinnvoll sein aus einer numerischen Outcomevariablen eine binäre Outcomevariable zu erzeugen. Dann können wir wieder eine Klassifikation rechnen. Aber auch hier musst du überlegen, ob das binäre Outcome dann dem numerischen Outcome inhaltlich entspricht. Wir können natürlich aus dem numerischen Lichteinfall die binäre Variable wenig/viel Licht transformieren. Dann muss die neue binäre Variable aber auch zur Fragestellung passen. Oder aus Noten auf der Likert-Skala nur zwei Noten mit schlecht/gut erschaffen.

Mehr Information zu Step Functions - Discretization

Wir wollen jetzt die Spalten age und crp in mindestens drei gleich große Gruppen aufspalten. Wenn wir mehr Gruppen brauchen, dann werden es mehr Gruppen werden. Das wichtige ist hier, dass wir gleich große Gruppen haben wollen.

pig_discrete_rec <- pig_rec |>
  step_discretize(crp, age, min_unique = 3)

pig_discrete_rec 
── Recipe ──────────────────────────────────────────────────────────────────────
── Inputs 
Number of variables by role
outcome:   1
predictor: 4
ID:        1
── Operations 
• Discretize numeric variables from: crp and age

Und dann können wir uns auch schon die Daten generieren. Wir immer gibt es noch andere Möglichkeiten um aus einer numerischen Spalte eine nominale Spalte zu generieren. Du musst dann abgleichen, welche Variante dir am besten passt.

pig_discrete_tbl <- pig_discrete_rec  |>
  prep() |>
  juice() 
Warning: Note that the options `prefix` and `labels` will be applied to all
variables.

Wir sehen, dass wir dann jeweils vier bins erhalten mit gut 25% Beobachtungen in jedem bin. Wir können dann mit der neuen Variable weiterrechnen und zum Beispiel diese neue nominale Variable dann in eine Dummykodierung umwandeln. Hier siehst du, dass du gewisse Schritte in einem Rezept in der richtigen Reihenfolge durchführen musst.

pig_discrete_tbl |> pull(crp) |> tabyl()
 pull(pig_discrete_tbl, crp)   n   percent
                        bin1 104 0.2524272
                        bin2 102 0.2475728
                        bin3 103 0.2500000
                        bin4 103 0.2500000
pig_discrete_tbl |> pull(age) |> tabyl()
 pull(pig_discrete_tbl, age)   n   percent
                        bin1 122 0.2961165
                        bin2 103 0.2500000
                        bin3  99 0.2402913
                        bin4  88 0.2135922

67.11 Korrelation zwischen Variablen

Als einer der letzten Schritte für die Aufreinigung der Daten schauen wir uns die Korrelation an. Du kannst dir die Korrelation im Kapitel Kapitel 40 nochmal näher anlesen. Wie schon bei der Imputation kann ich nur davon abraten einfach so den Filter auf die Daten anzuwenden. Es ist besser sich die numerischen Variablen einmal zu visualisieren und die Korrelation einmal zu berechnen. Das blinde Filtern von Variablen macht auf jeden Fall keinen Sinn!

Mehr Information zu High Correlation Filter

In der Klassifikation müssen wir schauen, dass wir keine numerischen Variablen haben, die im Prinzip das gleiche Aussagen also hoch miteinander korreliert sind. Die Variablen müssen wir dann entfernen. Oder besser eine von den beiden Variablen. Wir können den Schritt mit der Funktion step_corr() durchführen und einen Threshold für die Entfernung von numerischen Variablen festlegen. Wir nehmen hier ein \(\rho = 0.5\). Nochmal, das ist nicht sehr gut blind Vairablen zu entfernen. Schaue dir vorher einen paarweisen Korrelationsplot an und entscheide dann, ob du und welche Variablen du entfernen möchtest.

pig_corr_rec <- pig_rec |> 
  step_corr(all_numeric_predictors(), threshold = 0.5)

pig_corr_rec 
── Recipe ──────────────────────────────────────────────────────────────────────
── Inputs 
Number of variables by role
outcome:   1
predictor: 4
ID:        1
── Operations 
• Correlation filter on: all_numeric_predictors()

Dann können wir auch schon die Daten generieren. In unserem Fall wurde keine Variable entfernt. Die Korrelation untereinander ist nicht so groß. Wir runden hier wieder, damit sich die Tabelle nicht so in die Breite auf der Webseite entwickelt.

pig_corr_tbl <- pig_corr_rec  |>
  prep() |>
  juice() |> 
  mutate(across(where(is.numeric), round, 2)) 

67.12 Beispiel Gummibärchendaten

Schauen wir uns ein Rezept einmal in einem Rutsch auf den Gummibärchendaten einmal an. Wir müssen natürlich erstmal alle nominalen Variablen auch als solche umwandeln. Wir erschaffen also die passenden Faktoren für das Geschlecht und den Lieblingsgeschmack. Dann erschaffen wir noch eine ID für die Studierenden. Am Ende wählen wir noch ein paar Spalten aus, damit wir nicht alle Variablen vorliegen haben. Sonst wird der endgültige Datensatz sehr breit. Wir entfernen dann noch alle Beobachtungen aus den Daten, die einen fehlenden Wert bei dem Geschlecht haben. Das machen wir immer für die Variable, die dann unser Outcome sein soll.

gummi_tbl <- read_excel("data/gummibears.xlsx") |> 
  mutate(gender = as_factor(gender),
         most_liked = as_factor(most_liked),
         student_id = 1:n()) |> 
  select(student_id, gender, most_liked, age, semester, height) |>  
  drop_na(gender)

In Tabelle 67.5 sehen wir dann die Daten nochmal vor dem Preprocessing dargestellt. Wir sind nicht an den ursprünglichen Daten interessiert, da wir nur die Spalte gender vorhersagen wollen. Wir wollen hier keine Effekt schätzen oder aber Signifikanzen berechnen. Unsere Features dienen nur der Vorhersage des Labels. Wie die Features zahlenmäßig beschaffen sind, ist uns egal.

Tabelle 67.5— Auszug aus dem Daten zu den Gummibärchendaten.
student_id gender most_liked age semester height
1 m lightred 35 10 193
2 w yellow 21 6 159
3 w white 21 6 159
4 w white 36 10 180
5 m white 22 3 180
7 m green 22 3 180
778 m darkred 24 2 193
779 m white 27 2 189
780 m darkred 24 2 187
781 m green 24 2 182
782 w white 23 2 170
783 w green 24 2 180

Wir erschaffen uns nun das Rezept in dem wie definieren, dass das gender unser Label ist und der Rest der Vairablen unsere Features. Da wir noch die Spalte student_id haben, geben wir dieser Spalte noch die Rolle ID. Wir können dann in den Rezeptschritten dann immer diese Rolle ID aus dem Prozess der Transformation ausschließen.

gummi_rec <- recipe(gender ~ ., data = gummi_tbl) |> 
  update_role(student_id, new_role = "ID")

gummi_rec |> summary()
# A tibble: 6 × 4
  variable   type      role      source  
  <chr>      <list>    <chr>     <chr>   
1 student_id <chr [2]> ID        original
2 most_liked <chr [3]> predictor original
3 age        <chr [2]> predictor original
4 semester   <chr [2]> predictor original
5 height     <chr [2]> predictor original
6 gender     <chr [3]> outcome   original

Und dann haben wir hier alle Schritte einmal zusammen in einem Block. Wir imputieren die fehlenden Werte für die numerischen und nominalen Variablen getrennt. Dann verwandeln wir das Semester in mindestens vier Gruppen. Im nächsten Schritt werden dann alle numerischen Variablen auf eine Spannweite von \([0;1]\) gebracht. Wir erschaffen dann noch die Dummies für die nominalen Daten. Am Ende wollen wir dann alle Variablen mit fast keiner Varianz entfernen. Wir wollen dann immer die Spalte ID aus den Schritten ausschließen. Wir machen das mit der Funktion has_role() und dem - vor der Funktion. Damit schließen wir die Rolle ID aus dem Transformationsschritt aus.

gummi_full_rec <- gummi_rec |> 
  step_impute_mean(all_numeric_predictors(), -has_role("ID")) |> 
  step_impute_bag(all_nominal_predictors(), -has_role("ID")) |> 
  step_discretize(semester, num_breaks = 3, min_unique = 4) |> 
  step_range(all_numeric_predictors(), min = 0, max = 1, -has_role("ID")) |> 
  step_dummy(all_nominal_predictors(), -has_role("ID")) |> 
  step_nzv(all_predictors(), -has_role("ID"))

gummi_full_rec
── Recipe ──────────────────────────────────────────────────────────────────────
── Inputs 
Number of variables by role
outcome:   1
predictor: 4
ID:        1
── Operations 
• Mean imputation for: all_numeric_predictors() and -has_role("ID")
• Bagged tree imputation for: all_nominal_predictors() and -has_role("ID")
• Discretize numeric variables from: semester
• Range scaling to [0,1] for: all_numeric_predictors() and -has_role("ID")
• Dummy variables from: all_nominal_predictors() and -has_role("ID")
• Sparse, unbalanced variable filter on: all_predictors() and -has_role("ID")

Dann können wir wieder unsere Daten generieren. Ich runde hier wieder, da wir schnell sehr viele Kommastellen produzieren. In der Anwendung machen wir das natürlich dann nicht.

gummi_class_tbl <- gummi_full_rec |>
  prep() |>
  juice() |> 
  mutate(across(where(is.numeric), round, 2)) 

In der Tabelle 67.6 können wir uns die transformierten Daten einmal anschauen. Wir sehen das zum einen die Variable student_id nicht transformiert wurde. Alle numerischen Spalten sind auf einer Spannweite zwischen 0 und 1. Das Geschlecht wurde nicht transformiert, da wir das Geschlecht ja als Outcome festgelegt haben. Dann kommen die Dummykodierungen für die nominalen Spalten des Lieblingsgeschmack und des Semesters.

Tabelle 67.6— Der transformierte Gummibärchendatensatz nach der Anwendung des Rezepts.
student_id age height gender most_liked_white most_liked_green most_liked_darkred most_liked_none semester_bin2 semester_bin3
1 0.48 0.79 m 0 0 0 0 0 1
2 0.2 0.21 w 0 0 0 0 0 1
3 0.2 0.21 w 1 0 0 0 0 1
4 0.5 0.57 w 1 0 0 0 0 1
5 0.22 0.57 m 1 0 0 0 1 0
6 0.22 0.57 m 0 1 0 0 1 0
694 0.26 0.79 m 0 0 1 0 1 0
695 0.32 0.72 m 1 0 0 0 1 0
696 0.26 0.69 m 0 0 1 0 1 0
697 0.26 0.6 m 0 1 0 0 1 0
698 0.24 0.4 w 1 0 0 0 1 0
699 0.26 0.57 w 0 1 0 0 1 0

Bis hierher haben wir jetzt die Rezepte nur genutzt um uns die Daten aufzuarbeiten. Das ist eigentlich nur ein Schritt in der Klassifikation. Mit der Funktion workflow() können wir dann Rezepte mit Algorithmen verbinden. Dann nutzen wir die Funktion fit() um verschiedene Daten auf den Workflow anzuwenden. Das musst du aber nicht tun. Du kannst die Rezepte hier auch verwenden um deine Daten einfach aufzuarbeiten und dann eben doch ganz normale Statistik drauf zu rechnen.