R Code [zeigen / verbergen]
::p_load(tidyverse, tidymodels, magrittr,
pacman
janitor, conflicted)
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.
recipes
nach schauen.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.
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.
Wir wollen folgende R Pakete in diesem Kapitel nutzen.
::p_load(tidyverse, tidymodels, magrittr,
pacman
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.
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.
<- read_excel("data/infected_pigs.xlsx") |>
pig_tbl mutate(pig_id = 1:n()) |>
select(pig_id, infected, age, crp, sex, frailty) |>
select(pig_id, infected, everything())
In Tabelle 73.1 siehst du nochmal einen Ausschnitt aus den Daten. Wir haben noch die ID mit eingefügt, damit wir einzelne Beobachtungen nachvollziehen können.
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.
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.
<- recipe(infected ~ ., data = pig_tbl) |>
pig_rec update_role(pig_id, new_role = "ID")
|> summary() pig_rec
# 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
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.
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.
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.
Mehr Information zu Create Traditional Dummy Variables
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 71.2 haben wir einen kleinen Ausschnitt unser Schweinedaten gegeben. Wir wollen zuerst die Spalte sex
in eine Dummycodierung umwandeln.
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 71.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.
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 71.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
.
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_rec |>
pig_dummy_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.
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.
Mehr Information zu Zero Variance Filter und Near-Zero Variance Filter
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_rec |>
pig_zero_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.
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.
Mehr Information zu Scaling Numeric Data sowie Centering Numeric Data und Center and Scale Numeric Data
<- pig_rec |>
pig_scale_center_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_rec |>
pig_scale_center_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
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.
Mehr Information zu Scaling Numeric Data to a Specific Range
<- pig_rec |>
pig_range_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
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_rec |>
pig_imp_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.
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_rec |>
pig_discrete_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_rec |>
pig_discrete_tbl 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.
|> pull(crp) |> tabyl() pig_discrete_tbl
pull(pig_discrete_tbl, crp) n percent
bin1 104 0.2524272
bin2 102 0.2475728
bin3 103 0.2500000
bin4 103 0.2500000
|> pull(age) |> tabyl() pig_discrete_tbl
pull(pig_discrete_tbl, age) n percent
bin1 122 0.2961165
bin2 103 0.2500000
bin3 99 0.2402913
bin4 88 0.2135922
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 44 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_rec |>
pig_corr_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_rec |>
pig_corr_tbl prep() |>
juice() |>
mutate(across(where(is.numeric), round, 2))
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.
<- read_excel("data/gummibears.xlsx") |>
gummi_tbl 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 71.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.
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.
<- recipe(gender ~ ., data = gummi_tbl) |>
gummi_rec update_role(student_id, new_role = "ID")
|> summary() gummi_rec
# 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_rec |>
gummi_full_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_full_rec |>
gummi_class_tbl prep() |>
juice() |>
mutate(across(where(is.numeric), round, 2))
In der Tabelle 71.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.
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.