Parcours
/
R
R
/
Exercices
/
Club de fromage
Club de fromage

Club de fromage

Exercice d'apprentissage

Introduction

Qu'est-ce que la programmation fonctionnelle ?

Pour ce qui nous intéresse, voici quelques caractéristiques clés :

  • Des fonctions pures, qui font correspondre les entrées aux sorties de façon cohérente et sans effets de bord.
  • Des données immuables.
  • Des fonctions de première classe, que l'on peut manipuler comme des valeurs au même titre que n'importe quel autre type.
  • Des fonctions d'ordre supérieur, qui prennent d'autres fonctions en arguments ou les renvoient comme valeur de retour.

Cependant, R n'est pas un langage purement fonctionnel (contrairement à Haskell, par exemple).

Considère R comme un langage pragmatique, qui offre (de plus en plus) des capacités fonctionnelles là où celles-ci constituent une façon simple et nette de faire le travail. En général, l'analyse de données est le but, et la programmation n'est qu'un moyen d'y parvenir.

Les pipes

L'utilisation de pipes |> pour connecter des fonctions est une technique de plus en plus courante dans plusieurs langages.

En bref :

  • Le résultat situé à gauche du pipe devient le premier argument de la fonction à droite (où « droite » ignore les retours à la ligne facultatifs).
  • Les arguments restants peuvent être inclus dans l'appel de fonction.
  • En R, le pipe doit pointer vers un appel de fonction, et pas seulement vers un nom, ce qui rend les parenthèses toujours obligatoires (cela diffère de certains autres langages).

Certains programmeurs trouvent utile de prononcer le pipe « then » lorsqu'ils relisent du code : faire l'étape 1 PUIS l'étape 2...

L'exemple ci-dessous illustre la syntaxe.

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"

Ce type de pipe a été ajouté à Base R dans la version 4.1.0, et tu peux le voir appelé « native pipe ».

L'éditeur RStudio (entre autres) utilise Ctrl-Shift-M comme raccourci clavier pour insérer un pipe avec les espaces adéquats.

Les fonctions apply

On a vu précédemment que de nombreuses fonctions en R sont « vectorisées » : elles opèrent sur des vecteurs entiers, sans qu'il soit nécessaire d'écrire des boucles explicites.

Les fonctions qui n'utilisent que des fonctions vectorisées sont elles-mêmes probablement vectorisées automatiquement. Dans un exemple trivial :

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

Cependant, des fonctions plus complexes peuvent ne pas accepter un vecteur en entrée, par exemple si elles contiennent des boucles ou des blocs if-else.

On a besoin d'une façon plus générale d'appliquer des fonctions à des vecteurs (semblable à map() dans certains autres langages).

Comme R dispose d'une grande variété de structures de données dans son langage de base, il possède aussi toute une famille de fonctions *apply() pour les manipuler. Plus techniquement, les programmeurs les appellent souvent « fonctions d'ordre supérieur » et les mathématiciens « fonctionnelles », parce qu'elles prennent des fonctions en arguments.

Pour l'instant, intéressons-nous à lapply() et sapply().

lapply()

« List-apply » est conçue pour travailler sur des entrées de type liste, mais les vecteurs seront silencieusement convertis en listes si nécessaire.

La sortie est une liste, il peut donc falloir la déballer pour obtenir un vecteur :

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

Les apprenants qui connaissent le concept Switch remarqueront qu'on pourrait aussi remplacer le switch scalaire par la fonction vectorisée recode_values() de la bibliothèque dplyr. La vectorisation, c'est bien, et le R moderne offre plusieurs façons de l'obtenir.

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 » fait quelque chose de semblable à lapply, mais essaie aussi de simplifier la valeur de retour : par exemple, un vecteur nommé au lieu d'une liste. Selon le contexte, il peut être nécessaire d'utiliser unname() :

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

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

La bibliothèque purrr

lapply et sapply sont simples, familières à la plupart des utilisateurs de R, et restent très populaires pour les vecteurs et les listes simples.

Base R possède aussi beaucoup d'autres fonctions apply. Fournir un remplacement plus simple et plus cohérent face à ce genre de confusion est l'un des objectifs clés du Tidyverse.

Il existe désormais le paquet purrr, qui offre plusieurs options supplémentaires comme map(), tout en améliorant la cohérence de sa conception et la clarté des messages d'erreur.

Moins contraint par la rétrocompatibilité que le R de base, ce paquet rapproche sensiblement R de la programmation fonctionnelle moderne telle qu'on la trouve dans d'autres langages.

purrr contient des fonctions nombreuses et variées, qui se prêtent difficilement à un résumé simple. Parmi les ressources qui aident à s'orienter dans cette bibliothèque :

Les nombreuses fonctions map

La fonction map() applique une fonction à chaque élément de l'entrée et renvoie toujours une liste.

Jusqu'ici, cela ressemble à lapply(), simplement renommée, et peut-être avec de meilleurs messages d'erreur.

Cependant, map() n'est qu'un membre d'une vaste famille.

Quelques questions à se poser pour choisir une fonction :

  • Que veux-tu renvoyer ? map() renvoie toujours une liste, mais si tu veux un type précis de vecteur atomique, utilise map_chr() pour les strings, map_int() pour les entiers, etc.
  • As-tu besoin de chaque indice de position de l'entrée ? Préfixe les noms de fonctions par i, comme imap().
  • Veux-tu traiter plusieurs entrées en parallèle ?
    • Remplace map par map2 dans le nom, comme dans map2_dbl() pour deux entrées (par exemple des valeurs et des pondérations, pour calculer une moyenne pondérée)
    • Remplace par pmap pour une liste d'entrées parallèles, comme dans pmap_chr(). Et oui, le parallélisme au niveau matériel est possible.
  • Veux-tu traiter tous les éléments de l'entrée ? Sinon, pense à map_if() pour appliquer un filtre par prédicat (booléen), à map_at() pour préciser des critères d'indice de position, à head_while() pour traiter l'entrée jusqu'à ce qu'un élément échoue à un prédicat.
  • Les données d'entrée sont-elles profondément imbriquées ? Il existe des fonctions pour cette situation.
  • Veux-tu que la valeur de retour soit toujours du même type que l'entrée ? Utilise les fonctions modify() au lieu de map, ou modify_tree() pour une application récursive.

Ces fonctions prennent les données d'entrée comme premier argument chaque fois que possible, pour garantir qu'elles fonctionnent bien avec les pipes.

De plus, purrr est (délibérément) plus pointilleux que Base R. Si tu demandes quelque chose de précis, les fonctions renverront exactement cela ou s'arrêteront avec un message d'erreur. C'est sévère, mais très efficace pour réduire les bogues difficiles à trouver.

Les fonctions reduce

Les fonctions map ont généralement une valeur de retour de même longueur que l'entrée.

D'autres fonctions réduisent la dimensionnalité des données, par exemple sum().

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

Dans le cas ci-dessus, on convertit un vecteur de longueur 100 en un entier de longueur 1. Nous verrons dans un prochain concept sur les matrices et les tableaux que la notion de réduction de dimension est plus générale.

La fonction sum() est intégrée (comme beaucoup d'autres fonctions statistiques). Cependant, on a besoin d'un moyen d'appliquer des fonctions de réduction de dimension arbitraires sur une structure de données, à l'aide d'une fonction d'ordre supérieur équivalente à map().

Dans plusieurs autres langages, on utilise des noms de fonctions comme fold, foldl et foldr. En R, la fonction pertinente est reduce(), qui correspond au framework MapReduce, bien connu (des amateurs d'algorithmes).

Le premier argument est, comme d'habitude, les données d'entrée. Le second argument est une fonction à deux arguments : souvent une fonction anonyme, mais parfois aussi simple qu'un opérateur arithmétique comme +.

# 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

Cela change-t-il quelque chose qu'on applique la fonction depuis le début ou depuis la fin de l'entrée ?

Pas avec les opérateurs associatifs comme + ou *. De toute évidence, (1 + 2) + 3 == 1 + (2 + 3).

En revanche, - et / ne sont pas associatifs :

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

Pour les fonctions où la direction compte, il existe un argument facultatif .dir.

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

Comme pour map() et map2(), il existe aussi une fonction reduce2() pour traiter deux vecteurs d'entrée en parallèle.

En R, on aime travailler avec des vecteurs. Supposons que, au lieu d'une seule valeur finale, tu veuilles voir toutes les étapes intermédiaires.

Pour cela, les fonctions correspondantes sont accumulate() et accumulate2(). La syntaxe est semblable à celle de reduce(), avec quelques arguments facultatifs supplémentaires pour préciser le type de sortie.

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

Certains types courants d'accumulation ont leurs propres fonctions. Le concept Vector Functions présentait cumsum() et cumprod() dans Base R, et la bibliothèque dplyr propose cummean() pour les moyennes numériques, cumall() et cumany() pour les vecteurs logiques.

Les fonctions filter

Souvent, on veut inclure (ou exclure) les éléments de l'entrée qui correspondent à un prédicat.

Dans le concept Vector Filtering, on a vu une façon idiomatique de procéder.

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

C'est très utilisé dans le R traditionnel, mais cela s'intègre mal aux pipelines fonctionnels modernes.

Par conséquent, purrr fournit un ensemble de fonctions de prédicat, qui suivent la convention d'arguments habituelle f(input, func, ...) pour bien fonctionner avec les pipes.

Les noms diffèrent un peu de ceux de la plupart des autres langages (des noms comme filter et drop ont déjà un sens tout à fait différent en R).

Les fonctions purrr correspondantes incluent keep() et discard(), plus plusieurs autres.

On a aussi every(), some() et none(), qui remplacent all() et any() pour renvoyer une seule valeur logique.

Instructions

Nous lançons un club de fromages, qui utilisera l'apprentissage automatique (ML) pour sélectionner de nouveaux fromages à proposer à nos clients amateurs de fromage, en se fondant sur leur historique et leurs goûts.

Les nouveaux membres doivent remplir une enquête initiale afin que nous puissions rassembler quelques données de base pour commencer. On a ainsi découvert qu'il existe un sous-ensemble de clients emphatiques qui manquent de nuance dans leurs critiques. Comme cela risque de biaiser irrémédiablement un algorithme plus nuancé, un algorithme distinct a été mis en place pour répondre à leurs besoins. On te demande de fournir quelques fonctions utilitaires pour traiter leurs données.

Note

S'il existe sans doute différentes façons de résoudre les tâches suivantes, chacune peut être résolue avec au plus deux fonctions d'ordre supérieur.

1. Classe les clients

Le système de notation repose sur une base de cinq étoiles, qui se compose simplement des entiers 1:5. Les clients emphatiques n'attribuent que des notes de 1 ou 5, et nous voulons savoir si un client présente ce comportement.

Implémente all_15(), qui prend un vecteur de notes et renvoie true si toutes les notes valent 1 ou 5, et false sinon.

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

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

2. Nomme les clients

Nous devons associer le nom du client (ou son ID) à ses notes.

Implémente name_customers(), qui prend un vecteur de noms et une liste de notes, et les « zippe » en une liste de listes.

Les éléments des listes internes doivent porter les noms name et rating.

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. Sépare les clients emphatiques

Nous devons séparer les clients les plus emphatiques des autres.

Implémente emphatics(), qui prend des clients et des notes. Elle renvoie une liste limitée à ceux qui n'utilisent que des notes de 1 ou 5 étoiles.`

# names and ratings as in the task 2 example

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

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

4. Convertis les notes en binaire

Comme les clients emphatiques n'utilisent que les notes 1 et 5, il sera plus commode, en termes de calcul, de les convertir en 0 et 1.

Implémente to_binary(), qui prend un vecteur de notes emphatiques. Elle renvoie des notes binaires, où 1 a été converti en 0 et 5 a été converti en 1.

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

5. Calcule les notes moyennes au fil du temps

Tu veux continuer à améliorer les recommandations pour chaque client (pas seulement les emphatiques) au fil du temps, à mesure que tu comprends mieux leurs goûts.

Implémente satisfactions(), qui prend un vecteur de notes. Elle renvoie un vecteur de notes moyennes cumulées jusqu'à ce point. Arrondis les résultats à 2 décimales.

Espérons que les notes continuent de grimper, pour le bonheur de nos clients !

satisfactions(c(1, 2, 2, 3))
#> [1] 1.00 1.50 1.67 2.00
Modifie via GitHub Le lien s'ouvre dans une nouvelle fenêtre ou un nouvel onglet
R Exercism

Prêt à commencer Club de fromage ?

Inscris-toi sur Exercism pour apprendre et maîtriser R avec 21 concepts111 exercices, et un vrai mentorat humain, le tout gratuitement.