Weblogs

Bakken met R

Kennisdelen is een van de speerpunten van het Rijks ICT Gilde. We moedigen onze tech consultants aan dit zoveel mogelijk te doen. Gerben Kooistra is vanuit het Rijks ICT Gilde aan de slag als Data Scientist bij het Centraal Justitieel Incassobureau (CJIB). In dit blog deelt hij hoe je met behulp van de recipes package data kunt bewerken.

Een van de belangrijkste stappen bij het ontwikkelen van machine learning modellen is het pre-processen van je data. Vanuit R zijn er verschillende manieren om dit goed te doen. Echter, bij de implementatie van een model in een productieomgeving blijft het altijd een uitdaging om ervoor te zorgen dat de data die als input aan het model wordt gegeven, exact verkregen en bewerkt is zoals dat ook gebeurd is ten tijde van het ontwikkelen van het model. Om dit probleem te tackelen biedt de recipes package een aantal mooie functionaliteiten.

De recipes package is een vrij nieuwe package binnen R en is onderdeel van tidymodels. Tidymodels berust op het concept van Tidy Data van Hadley Wickham’s paper Tidy Data. Met de recipe package kan men (zoals de naam al doet vermoeden) een recipe (blauwdruk) definiëren, die vervolgens gebruikt kan worden om volgens een aantal standaard stappen (in een vaste volgorde) data te coderen, pre-processen en feature engineering toe te passen. Hiermee vergroot je reproduceerbaarheid en kun je het coderen, pre-processen en feature engineering in de productieomgeving eenvoudiger implementeren. Dit klinkt misschien nog vrij vaag, maar het zal snel duidelijk worden met een voorbeeld.

Eerst packages installeren

Als eerste moeten we de packages die we gaan gebruiken natuurlijk installeren. Recipes is onderdeel van tidymodels: een verzameling packages om modellen te ontwikkelen volgens de tidy principes. Mijn advies zou daarom zijn om tidymodels als geheel te installeren, maar voor nu hebben we alleen de volgende vier packages nodig. Indien je alle vier al hebt geïnstalleerd kun je de installatiestap natuurlijk overslaan.

install.packages('rsample')
install.packages('recipes')
install.packages('dplyr')
install.packages('tidyr')

library('rsample')
library('recipes')
library('dplyr')
library('tidyr')

Vervolgens gaan we een dataset inladen waarbij we een klasse definiëren. In de praktijk is dit je ‘target variable’. Hiervoor gaan we gebruik maken van de mtcars dataset waarbij de ‘target variable’ zal aangeven of een bepaald model auto binnen 18 seconden vanuit stilstand een ¼ mijl kan afleggen. Daarnaast voegen we nog een ‘bedachte’ categorische variabele toe, genaamd gewichtsklasse.

Train- en test dataset

Als volgende stap splitsen we de data naar een fictieve train- en test dataset om te laten zien hoe je met de recipes package eenvoudig datapreparatie definities van je training dataset kunt toepassen op je test dataset.

df <- mtcars %>% mutate(class = ifelse(qsec<18, 'Ja', 'Nee'),
                        gewichtsklasse = ifelse(wt<2.6, 'Licht'
                                                ,ifelse(wt<3.2, 'Gemiddeld'
                                                        ,ifelse(wt<3.6, 'Zwaar', NA)))

                       ) %>% select(-qsec)

train_test_split <- initial_split(df)
df_train <- training(train_test_split)
df_test <- testing(train_test_split)

df_train$mpg[5] <- NA
df_test$mpg[5] <- NA
df_train$drat[5] <- NA
df_test$drat[5] <- NA
df_train$hp[5] <- NA
df_test$hp[5] <- NA
df_test$gewichtsklasse[5] <- 'Extreem'

We beschikken nu dus over een train- en test dataset, waarbij we een ‘target variable’ hebben, een aantal numerieke variabelen en een categoriale variabele met NA voorkomens. Verder hebben we voor zowel de train- als de test dataset voor de vijfde observatie de mpg-, drat- en hp-variabele de waarde NA gegeven, om straks te kunnen zien hoe recipes hiermee om kan gaan. Bij de test dataset hebben we voor observatie 5 ook nog een nieuwe categorie gedefinieerd die niet in de training dataset voorkomt. Ook dit doen we om te laten zien hoe recipes hier rekening mee kan houden.

Controleer alle uitgevoerde stappen eventueel door respectievelijk View(df), View(df_train) en View(df_test) uit te voeren.

Figuur 1 - Resultaat View(df_test)

Bewerkingsstappen

We gaan ons nu richten op de drie functies die de recipes package bijzonder maken, namelijk recipe(), prep() en bake(). Met de functie recipe() gaan we een recept definiëren op basis van de training dataset. Hierdoor wordt gedefinieerd welke rol variabelen hebben en welke bron deze hebben. Vervolgens kunnen we hieraan bewerkingsstappen toevoegen. Dit doen we met functies die de volgende opbouw hebben: step_naambewerking().

Binnen deze functie kunnen we aangegeven voor welke variabelen de bewerking moet worden uitgevoerd, en in sommige gevallen kunnen we ook hier een functie definiëren die uitgevoerd moet worden. Recipes geeft ons de mogelijkheid om hier bewerkingen uit te voeren voor alle variabelen die bijvoorbeeld numeriek of nominaal zijn met respectievelijk all_numeric() en all_nominal(). Alle bewerkingsstappen, waarvan mijns inziens heel veel geweldige stappen zoals step_knnimpute(), staan hier gedocumenteerd.

Wat we nu gaan doen is een recipe definiëren op de training dataset df_train. Daarna gaan we de volgende bewerkingsstappen uitvoeren:

  1. Voor alle numerieke variabelen, behalve de variabele hp, gaan we de ontbrekende waarden (NA) vullen met de waarde 0. Dit doen we met de functies step_mutate_at() en replace_na() van de tidyr package. Het selecteren van de juiste variabelen doen we met all_numeric().
  2. Voor de variabele hp gaan we de ontbrekende waarden (NA) vullen met het gemiddelde. Deze actie heet meanimpute en wordt uitgevoerd met de functie step_meanimpute().
  3. Vervolgens gaan we alle categorische variabelen (oftewel all_nominal()) omzetten naar een factor. Dit doen we met de functie step_factor2string().
  4. Daarna gaan we voor de variabele gewichtsklasse definiëren hoe met nieuwe categorieën en ontbrekende waarden moet worden omgegaan. Hiervoor gebruiken we de functie step_novel() en step_unknown().
  5. Een nieuwe variabele definiëren we vervolgens met de functie step_mutate() die de verhouding weergeeft tussen de variabelen hp en cyl.
  6. En als laatste stap gaan we deze nieuwe variabele (stap 5) normaliseren met de functie step_scale().

Kloppen we bovenstaande in een script in R, dan ziet dat er als volgt uit:

mtcars_recipe <- recipe(class~., data = df_train) %>%

  step_mutate_at(all_numeric(), -hp, fn = ~ tidyr::replace_na(., 0)) %>% #Stap 1
  step_meanimpute(hp) %>% #Stap 2

  step_factor2string(all_nominal()) %>% #Stap 3

  step_novel(gewichtsklasse, new_level = "Nieuw level") %>% #Stap 4
  step_unknown(gewichtsklasse, new_level = "Onbekend") %>% #Stap 4

  step_mutate(ratio_hp_cyl = hp/cyl,
              controle_ratio_hp_cyl = hp/cyl
              ) %>% #Stap 5

  step_scale(ratio_hp_cyl) #Stap 6

Het volgende wat we moeten doen is de functie prep() gebruiken. Hiermee wordt de gedefinieerde recipe (gemaakt hierboven) tegen de training dataset aangelegd en worden parameters etc. geschat voor als je later andere datasets met je recipe gaat bewerken (bake()). De functie is weer heel eenvoudig uit te voeren:

prepped_recipe_mtcars <- prep(mtcars_recipe,training = df_train)

Vervolgens kunnen we deze ‘prepped recipe’ toepassen op een dataset met de functie bake(). Dit doen we eerst op onze training dataset:

training_set <- bake(prepped_recipe_mtcars, df_train)

Met View(training_set) kun je de gecreëerde training dataset bekijken. Je zult zien dat voor observatie 5 voor de variabelen mpg en drat de waarde 0 is ingevoerd. Verder zien we dat voor de variabele hp bij observatie 5 de waarde gelijk is aan de gemiddelde waarde van de variabele hp voor de gehele training dataset. Controleer dit door de volgende code uit te voeren:

mean(training_set$hp[-5])
Figuur 2 - Resultaat View(training_set)

Als je de codes str(training_set$gewichtsklasse) en levels(training_set$gewichtsklasse) uitvoert, zie je dat de variabele is omgezet naar een factor en dat deze nu 5 levels heeft, waarvan één de waarde “Onbekend” heeft, en deze in de dataset de plek heeft ingenomen van de NA-waardes. Daarnaast zie je dat het level “Nieuw level” wel vastgelegd staat, maar niet voorkomt. Onthoud dit. Kijk je naar de kolommen ratio_hp_cyl en controle_ratio_hp_cyl dan zie je dat stap 6 is uitgevoerd op de variabele ratio_hp_cyl ten opzichte van de gecreëerde controlevariabele controle_ratio_hp_cyl.

Reproduceerbaarheid

In praktijksituaties hebben we nu een training dataset gecreëerd met een aantal pre-processing stappen en hebben we aan feature engineering gedaan. Een vervolgstap zou zijn om hier nu een machine learning model op te trainen. Bij het trainen van modellen wil je altijd garanderen dat data in de test dataset, en later in productie, op dezelfde manier wordt bewerkt als in de training dataset. Op deze situatie is immers het model getraind. We hebben een aantal stappen gedaan, zoals het normaliseren en afvangen van lege waarden (NA). In de praktijk wil je de waarden waarmee je normaliseert en imputeert op je training dataset ook gebruiken voor je test dataset (en later in productie). Dit betekent dus dat bij de test dataset de variabelen mpg en drat gewoon met 0 gevuld worden, maar dat bijvoorbeeld de lege hp-waarde die we gecreëerd hebben bij observatie 5, wordt gevuld met het gemiddelde van de waarde van de variabele van de training dataset.

Daarnaast heeft in onze test dataset voor observatie 5 de gewichtsklasse een waarde die op basis van de training dataset nog niet is voorgekomen, namelijk de waarde ‘Extreem’. In praktijk weet een getraind model vaak dus niet hoe hiermee moet worden omgegaan en moet deze daarom op een juiste manier worden afgevangen. Of er moet gekozen worden om voor deze observatie geen voorspelling te doen.

We gaan de bake() functie uitvoeren op onze test dataset:

testing_set <- bake(prepped_recipe_mtcars, df_test)

Hiervoor moeten we op een aantal punten controleren, namelijk:

  1. Zijn voor de variabelen mpg en drat de waarde 0 ingevuld voor observatie 5?
  2. Is voor de variabele hp bij observatie 5 het gemiddelde ingevuld (van de variabele hp) van onze training dataset?
  3. Is voor de gewichtsklasse ‘Extreem’ een juiste afvang gemaakt en is deze omgezet naar de factor level ‘Nieuw level’?

Hiervoor gaan we een aantal controles uitvoeren. Om te controleren of punt 1 juist is:

print(testing_set$mpg[5] == 0)

print(testing_set$drat[5] == 0)

Beide keren krijgen we vanuit R het resultaat TRUE terug.

Voor controle 2 voeren we de volgende code uit:

print(testing_set$hp[5] == mean(training_set$hp[-5]))

Ook hier geeft R het resultaat TRUE.

Voor punt 3 voeren we testing_set$gewichtsklasse[5] uit. Het resultaat van R is dat deze observatie de waarde “Nieuw level” heeft. Afhankelijk van jouw persoonlijke wens kun je met bijvoorbeeld de functie step_mutate() ervoor zorgen dat het nieuwe level altijd de waarde “Onbekend” krijgt en je hiervoor een voorspelling gaat doen. Ook kan je ervoor kiezen om deze observaties niet mee te nemen en bijvoorbeeld uit te sluiten met de stap step_filter(). Ervoor zorgen dat je nooit observaties meeneemt waarin nieuwe levels voorkomen bij de variabele gewichtsklasse, doe je door de volgende stap toe te voegen:

step_filter(gewichtsklasse != 'Nieuw level')  #(Niet de %>% vergeten!)

Ik hoop dat hiermee duidelijk is geworden hoe de recipes package een mooie toevoeging is voor het bewerken van data, waarmee je kunt garanderen dat data in je validatie dataset, test dataset en later in productie op dezelfde manier is verwerkt als de data waar het te gebruiken model op is getraind. Doordat de recipes package in een aantal eenvoudige stappen zelf een complete verwerkingsmatrix opslaat, scheelt dit jou als professional veel werk. Ook wordt hiermee de reproduceerbaarheid een stuk eenvoudiger en worden juiste definities gegarandeerd. In mijn voorbeeld komen maar enkele functies van de recipes aan bod. Het loont dan ook zeker om een paar uur deze package te onderzoeken.

Reactie toevoegen

U kunt hier een reactie plaatsen. Ongepaste reacties worden niet geplaatst. Uw reactie mag maximaal 2000 karakters tellen.

* verplichte velden

Uw reactie mag maximaal 2000 karakters lang zijn.

Reacties

Er zijn nu geen reacties gepubliceerd.