Tracks
/
R
R
/
Übungen
/
Käseclub
Käseclub

Käseclub

Lernübung

Einführung

Was ist funktionale Programmierung?

Für unsere Zwecke sind das einige zentrale Merkmale:

  • Reine Funktionen, die Eingaben konsistent auf Ausgaben abbilden und keine Nebenwirkungen haben.
  • Unveränderliche Daten.
  • Funktionen als Werte erster Klasse, die sich wie jeder andere Typ als Wert behandeln lassen.
  • Funktionen höherer Ordnung, die andere Funktionen als Argumente oder als Rückgabewerte entgegennehmen.

R ist jedoch keine reine funktionale Sprache (im Gegensatz etwa zu Haskell).

Sieh R als pragmatische Sprache, die (zunehmend) funktionale Möglichkeiten bietet, wo diese ein sauberer und einfacher Weg sind, eine Aufgabe zu erledigen. Im Allgemeinen ist die Datenanalyse das Ziel und das Programmieren nur ein Mittel zu diesem Zweck.

Pipes

Die Verwendung von Pipes |> zum Verbinden von Funktionen ist eine immer verbreitetere Technik in mehreren Sprachen.

Kurz gesagt:

  • Ein Ergebnis links von der Pipe wird zum ersten Argument der Funktion rechts (wobei „rechts“ optionale Zeilenumbrüche ignoriert).
  • Alle übrigen Argumente können im Funktionsaufruf angegeben werden.
  • In R müssen wir auf einen Funktions_aufruf_ pipen, nicht nur auf einen Namen, deshalb sind runde Klammern immer erforderlich (das unterscheidet sich von einigen anderen Sprachen).

Manche Programmierer finden es hilfreich, die Pipe beim Lesen von Code als „dann“ auszusprechen: mache Schritt 1, DANN mache Schritt 2...

Das Beispiel unten zeigt die Syntax.

library(stringr)

"Monday-25-February" |> 
  str_split("-") |>     
  unlist() |>               # [1] "Monday"   "25"       "February"
  str_sub(1, 3) |>          # [1] "Mon" "25"  "Feb"
  str_to_upper() |>         # [1] "MON" "25"  "FEB"
  str_flatten(collapse = ", ")
#> [1] "MON, 25, FEB"

Diese Art von Pipe wurde in R 4.1.0 in Base R eingeführt, und manchmal wird sie als „native Pipe“ bezeichnet.

Der RStudio-Editor (unter anderem) verwendet Ctrl-Shift-M als Tastenkürzel, um eine Pipe mit passenden Leerzeichen einzufügen.

Apply-Funktionen

Wir haben vorhin gesehen, dass viele Funktionen in R „vektorisiert“ sind: Sie arbeiten über ganze Vektoren hinweg, ohne dass du explizite Schleifen schreiben musst.

Funktionen, die nur vektorisierte Funktionen verwenden, sind selbst wahrscheinlich automatisch vektorisiert. Ein einfaches Beispiel:

triple <- function(x) return(3 * x)
triple(1:10)
 [1]  3  6  9 12 15 18 21 24 27 30

Komplexere Funktionen akzeptieren jedoch möglicherweise keine Vektoreingabe, zum Beispiel wenn sie Schleifen oder if-else-Blöcke enthalten.

Wir brauchen eine allgemeinere Möglichkeit, Funktionen auf Vektoren anzuwenden (ähnlich wie map() in manchen anderen Sprachen).

Da R in der Basissprache eine große Vielfalt an Datenstrukturen bietet, gibt es dort auch eine ganze Familie von *apply()-Funktionen, um mit ihnen zu arbeiten. Genauer gesagt werden sie von Programmierern oft „Funktionen höherer Ordnung“ und von Mathematikern „Funktionale“ genannt, weil sie Funktionen als Argumente entgegennehmen.

Betrachten wir zunächst lapply() und sapply().

lapply()

„List-apply“ ist für Listeneingaben gedacht, aber Vektoren werden bei Bedarf stillschweigend in Listen umgewandelt.

Die Ausgabe ist eine Liste, deshalb muss sie möglicherweise entpackt werden, um einen Vektor zu erhalten:

h <- function(size) {
    switch(size,
           "small" = 6,
           "medium" = 8,
           "large" = 10
    )
}

v <- c("small", "large", "medium")

h(v) # bad use of an unvectorized function
#> Error in switch(size, small = 6, medium = 8, large = 10) : 
#>   EXPR must be a length 1 vector

lapply(v, h)  # output is a list
#> [[1]]
#> [1] 6

#> [[2]]
#> [1] 10

#> [[3]]
#> [1] 8

lapply(v, h) |> unlist() # output is a vector
#> [1]  6 10  8

Wer mit dem Switch-Konzept vertraut ist, dem fällt vielleicht auf, dass wir das skalare switch alternativ durch das vektorisierte recode_values() aus der Bibliothek dplyr ersetzen könnten. Vektorisierung ist gut, und modernes R bietet mehrere Wege, sie zu erreichen.

library(dplyr)

h <- function(size) {
  recode_values(size,
         "small" ~ 6,
         "medium" ~ 8,
         "large" ~ 10
  )
}

v <- c("small", "large", "medium")
h(v)
#> [1]  6 10  8

sapply()

„Simplified-apply“ macht etwas Ähnliches wie lapply, versucht aber zusätzlich, den Rückgabewert zu vereinfachen: zum Beispiel einen benannten Vektor statt einer Liste. Je nach Zusammenhang kann die Verwendung von unname() nötig sein:

sapply(v, h)
 small  large medium 
     6     10      8 

sapply(v, h) |> unname()
#> [1]  6 10  8

Die Bibliothek purrr

lapply und sapply sind einfach, den meisten R-Nutzern vertraut und für Vektoren und einfache Listen noch immer recht beliebt.

Base R hat außerdem viele weitere Apply-Funktionen. Ein zentrales Ziel des Tidyverse ist es, für diese Art von Verwirrung einen einfacheren und konsistenteren Ersatz zu bieten.

Inzwischen gibt es das Paket purrr, das mehrere zusätzliche Möglichkeiten wie map() bietet und zugleich das Design konsistenter und die Fehlermeldungen klarer macht.

Da es weniger durch Abwärtskompatibilität eingeschränkt ist als Base R, bringt dieses Paket R deutlich näher an die moderne funktionale Programmierung, wie sie in anderen Sprachen zu finden ist.

purrr enthält viele unterschiedliche Funktionen, die sich kaum kurz zusammenfassen lassen. Hilfreiche Ressourcen, um sich in der Bibliothek zurechtzufinden, sind:

Die vielen map-Funktionen

Die Funktion map() wendet eine Funktion auf jedes Element der Eingabe an und gibt immer eine Liste zurück.

Bisher klingt das wie lapply(), nur umbenannt und vielleicht mit besseren Fehlermeldungen.

map() ist jedoch nur ein Mitglied einer großen Familie.

Fragen, die dir bei der Wahl einer Funktion helfen können:

  • Was möchtest du zurückgeben? map() gibt immer eine Liste zurück, aber wenn du einen bestimmten Typ von atomarem Vektor möchtest, verwende map_chr() für Strings, map_int() für Ganzzahlen usw.
  • Brauchst du jeden Positionsindex aus der Eingabe? Stelle den Funktionsnamen ein i voran, etwa imap().
  • Möchtest du mehrere Eingaben parallel verarbeiten?
    • Ersetze map im Namen durch map2, wie in map2_dbl() für zwei Eingaben (z. B. Werte und Gewichte, um einen gewichteten Mittelwert zu berechnen)
    • Ersetze es durch pmap für eine Liste paralleler Eingaben, wie in pmap_chr(). Und ja, Parallelität auf Hardwareebene ist möglich.
  • Möchtest du alle Elemente der Eingabe verarbeiten? Wenn nicht, denk an map_if(), um einen Prädikatfilter (boolesch) anzuwenden, an map_at(), um Kriterien für Positionsindizes anzugeben, und an head_while(), um die Eingabe zu verarbeiten, bis ein Element ein Prädikat nicht erfüllt.
  • Sind die Eingabedaten tief verschachtelt? Dafür gibt es Funktionen.
  • Soll der Rückgabewert immer denselben Typ wie die Eingabe haben? Verwende die modify()-Funktionen statt map, oder modify_tree() für rekursive Anwendung.

Diese Funktionen haben, wann immer möglich, die Eingabedaten als erstes Argument, damit sie gut mit Pipes funktionieren.

Außerdem ist purrr (absichtlich) pedantischer als Base R. Wenn du etwas Bestimmtes verlangst, geben die Funktionen genau das zurück oder brechen mit einer Fehlermeldung ab. Hart, aber sehr wirksam, um schwer auffindbare Fehler zu reduzieren.

Reduce-Funktionen

Map-Funktionen haben im Allgemeinen einen Rückgabewert derselben Länge wie die Eingabe.

Manche anderen Funktionen reduzieren die Dimensionalität der Daten, zum Beispiel sum().

sum(1:100)
#> [1] 5050

Im obigen Fall wandeln wir einen Vektor der Länge 100 in eine Ganzzahl der Länge 1 um. In einem späteren Konzept zu Matrizen und Arrays werden wir sehen, dass das Konzept der Dimensionsreduktion allgemeiner ist.

Die Funktion sum() ist eingebaut (wie viele andere statistische Funktionen). Wir brauchen jedoch eine Möglichkeit, beliebige dimensionsreduzierende Funktionen auf eine Datenstruktur anzuwenden, und zwar mit einer Funktion höherer Ordnung, die map() entspricht.

In mehreren anderen Sprachen werden Funktionsnamen wie fold, foldl und foldr verwendet. In R ist die relevante Funktion reduce(), die dem (Algorithmus-Fans wohlbekannten) MapReduce-Framework entspricht.

Das erste Argument sind wie üblich die Eingabedaten. Das zweite Argument ist eine Funktion mit zwei Argumenten: oft eine anonyme Funktion, aber möglicherweise so einfach wie ein arithmetischer Operator wie +.

# surround an infix operator with backticks, to use as a function
reduce(1:100, `+`)
#> [1] 5050  # same as sum(1:100)

# anonymous function, alternately add and subtract
reduce(1:10, \(accum, nextval) ifelse(nextval %% 2 == 0, accum + nextval, accum - nextval))
#> [1] 7

Spielt es eine Rolle, ob wir die Funktion vom Anfang oder vom Ende der Eingabe aus anwenden?

Bei assoziativen Operatoren wie + oder * nicht. Klar, (1 + 2) + 3 == 1 + (2 + 3).

Im Gegensatz dazu sind - und / nicht assoziativ:

(1 - 2) - 3
#> [1] -4
1 - (2 - 3)
#> [1] 2

Für Funktionen, bei denen die Richtung eine Rolle spielt, gibt es ein optionales Argument .dir.

# default is .dir = "forward"
reduce(1:10, `-`)
#> [1] -53
reduce(1:10, `-`, .dir = "backward")
#> [1] -5

Wie bei map() und map2() gibt es außerdem eine Funktion reduce2(), um zwei Eingabevektoren parallel zu verarbeiten.

In R arbeiten wir gern mit Vektoren. Angenommen, du möchtest statt eines einzelnen Endwerts alle Zwischenschritte sehen.

Dafür gibt es die entsprechenden Funktionen accumulate() und accumulate2(). Die Syntax ähnelt der von reduce(), mit einigen zusätzlichen optionalen Argumenten zur Angabe des Ausgabetyps.

accumulate(1:10, `+`)
#> [1]  1  3  6 10 15 21 28 36 45 55

Einige häufige Arten des Accumulate haben ihre eigenen Funktionen. Das Vektorfunktionen-Konzept behandelte cumsum() und cumprod() in Base R, und die Bibliothek dplyr hat cummean() für numerische Mittelwerte sowie cumall() und cumany() für logische Vektoren.

Filter-Funktionen

Häufig möchten wir Elemente der Eingabe einbeziehen (oder ausschließen), die zu einem Prädikat passen.

Im Vektorfilterung-Konzept haben wir eine idiomatische Möglichkeit dafür gesehen.

v <- 1:3
v[v >= 2]
#> [1] 2 3

Das wird im traditionellen R viel verwendet, passt aber schlecht in moderne funktionale Pipelines.

Daher bietet purrr eine Reihe von Prädikatfunktionen, die der üblichen Argumentkonvention f(input, func, ...) folgen, damit sie mit Pipes funktionieren.

Die Namen unterscheiden sich etwas von denen der meisten anderen Sprachen (Namen wie filter und drop bedeuten in R bereits etwas völlig anderes).

Zu den entsprechenden purrr-Funktionen gehören keep() und discard() sowie einige weitere.

Außerdem gibt es every(), some() und none(), die all() und any() ersetzen, wenn ein einzelner logischer Wert zurückgegeben werden soll.

Anleitung

Wir gründen einen Käseclub, der maschinelles Lernen (ML) einsetzt, um neue Käsesorten auszuwählen, die wir unseren käseliebenden Kunden auf Basis ihrer Historie und ihres Geschmacks anbieten können.

Neue Mitglieder müssen eine erste Umfrage ausfüllen, damit wir zu Beginn ein paar grundlegende Daten sammeln können. Dabei hat sich herausgestellt, dass es eine Untergruppe von emphatischen Kunden gibt, denen es an Nuancen in ihren Kritiken fehlt. Da dies einen differenzierteren Algorithmus unwiderruflich verzerren kann, gibt es einen separaten Algorithmus, der auf ihre Bedürfnisse zugeschnitten ist. Du sollst einige Hilfsfunktionen bereitstellen, um ihre Daten aufzubereiten.

Note

Auch wenn es verschiedene Wege geben mag, die folgenden Aufgaben zu lösen, lässt sich jede mit höchstens zwei Funktionen höherer Ordnung lösen.

1. Kunden klassifizieren

Das Bewertungssystem beruht auf einer Fünf-Sterne-Basis und besteht einfach aus den Ganzzahlen 1:5. Die emphatischen Kunden vergeben nur Bewertungen von 1 oder 5, und wir wollen wissen, ob ein Kunde dieses Verhalten zeigt.

Implementiere all_15(), die einen Vektor von Bewertungen entgegennimmt und true zurückgibt, wenn alle Bewertungen entweder 1 oder 5 sind, und andernfalls falsch.

all_15(c(2, 3, 4, 4, 1))
#> [1] FALSE

all_15(c(1, 5, 5, 1, 5))
#> [1] TRUE

2. Kunden benennen

Wir müssen den Kundennamen (oder die ID) mit den jeweiligen Bewertungen verknüpfen.

Implementiere name_customers(), die einen Vektor von Namen und eine Liste von Bewertungen entgegennimmt und sie zu einer Liste von Listen „zippt“.

Die inneren Listenelemente sollten die Namen name und rating haben.

names <- c("c1", "c2")
ratings <- list(c(2, 3, 5), c(1, 1, 5))
name_customers(names, ratings)
#> [[1]]
#> [[1]]$name
#> [1] "c1"

#> [[1]]$rating
#> [1] 2 3 5

#> [[2]]
#> [[2]]$name
#> [1] "c2"

#> [[2]]$rating
#> [1] 1 1 5

3. Emphatische Kunden herausfiltern

Wir müssen die emphatischeren Kunden von den anderen trennen.

Implementiere emphatics(), die Kunden und Bewertungen entgegennimmt. Sie gibt eine Liste zurück, die auf diejenigen beschränkt ist, die nur Bewertungen mit 1 oder 5 Sternen abgeben.

# names and ratings as in the task 2 example

emphatics(names, ratings)
#> [[1]]
#> [[1]]$name
#> [1] "c2"

#> [[1]]$rating
#> [1] 1 1 5

4. Bewertungen in Binärwerte umwandeln

Da die emphatischen Kunden nur die Bewertungen 1 und 5 verwenden, ist es recheneffizienter, diese in 0 und 1 umzuwandeln.

Implementiere to_binary(), die einen Vektor emphatischer Bewertungen entgegennimmt. Sie gibt binäre Bewertungen zurück, wobei 1 in 0 und 5 in 1 umgewandelt wurde.

to_binary(c(1, 1, 5, 5, 1))
#> [1] 0 0 1 1 0

5. Durchschnittliche Bewertungen über die Zeit berechnen

Du möchtest die Empfehlungen für alle einzelnen Kunden (nicht nur für die emphatischen) mit der Zeit immer weiter verbessern, während du ihren Geschmack besser verstehst.

Implementiere satisfactions(), die einen Vektor von Bewertungen entgegennimmt. Sie gibt einen Vektor der kumulativen Durchschnittsbewertungen bis zu diesem Punkt zurück. Runde die Ergebnisse auf 2 Nachkommastellen.

Hoffentlich steigen die Zahlen immer weiter, für zufriedene Kunden!

satisfactions(c(1, 2, 2, 3))
#> [1] 1.00 1.50 1.67 2.00
Über GitHub bearbeiten Der Link öffnet sich in einem neuen Fenster oder Tab
R Exercism

Bereit, mit Käseclub zu starten?

Melde dich bei Exercism an, um R mit 21 Konzepte111 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.