5  Programmation

Maintenant que nous avons appris les bases, la prochaine étape importante dans votre aventure sur R est… la programmation !

Il existe déjà un grand nombre de paquets R disponibles, ce qui est surement plus que suffisant pour couvrir tout ce que vous pourriez vouloir faire ! Alors, pourquoi alors créer ses propres fonctions R ? Pourquoi ne pas s’en tenir aux fonctions d’un paquet ?

Eh bien, dans certains cas, vous voudrez personnaliser ces fonctions pré-existantes pour répondre à vos besoins spécifiques. Il se peut aussi que vous souhaitiez mettre en œuvre une nouvelle approche, ce qui signifie qu’il n’y aura pas de paquets déjà développés qui fonctionneront pour vous (Bon, ces deux cas de figure ne sont pas particulièrement courants).

Les fonctions sont principalement utilisées pour faire une chose de manière simple sans avoir à taper le code nécessaire à chaque fois (ce qui n’est pas très intéressant et peut rapidement surcharger votre script). On peut considérer les fonctions comme un raccourci pour copier-coller.

Si vous devez effectuer une tâche similaire quatre fois ou plus, créez une fonction qui exécute cette tâche, et appelez-la simplement quatre fois, ou encore mieux : appelez-la dans une boucle !

5.1 Regarder derrière le rideau

Une bonne façon de commencer à apprendre à programmer en R est de regarder ce que d’autres ont fait avant : Alors, commençons par jeter un bref coup d’œil derrière le rideau !

Pour beaucoup de fonctions en R, si vous voulez jeter un coup d’œil rapide à la machinerie en coulisses, nous pouvons simplement écrire le nom de la fonction, mais sans l’attribut ().

Notez que l’affichage du code source des paquets R de base (ceux qui sont pré-installés avec R) nécessite quelques étapes supplémentaires que nous ne couvrirons pas ici (voir ce lien si cela vous intéresse), mais pour la plupart des autres paquets que vous installez vous-même, il suffit généralement d’entrer le nom de la fonction sans la mention() affichera le code source de la fonction.

Vous pouvez regarder comment la fonction d’ajustement d’un modèle linéaire lm() est construite :

lm
function (formula, data, subset, weights, na.action, method = "qr", 
    model = TRUE, x = FALSE, y = FALSE, qr = TRUE, singular.ok = TRUE, 
    contrasts = NULL, offset, ...) 
{
    ret.x <- x
    ret.y <- y
    cl <- match.call()
    mf <- match.call(expand.dots = FALSE)
    m <- match(c("formula", "data", "subset", "weights", "na.action", 
        "offset"), names(mf), 0L)
    mf <- mf[c(1L, m)]
    mf$drop.unused.levels <- TRUE
    mf[[1L]] <- quote(stats::model.frame)
    mf <- eval(mf, parent.frame())
    if (method == "model.frame") 
        return(mf)
    else if (method != "qr") 
        warning(gettextf("method = '%s' is not supported. Using 'qr'", 
            method), domain = NA)
    mt <- attr(mf, "terms")
    y <- model.response(mf, "numeric")
    w <- as.vector(model.weights(mf))
    if (!is.null(w) && !is.numeric(w)) 
        stop("'weights' must be a numeric vector")
    offset <- model.offset(mf)
    mlm <- is.matrix(y)
    ny <- if (mlm) 
        nrow(y)
    else length(y)
    if (!is.null(offset)) {
        if (!mlm) 
            offset <- as.vector(offset)
        if (NROW(offset) != ny) 
            stop(gettextf("number of offsets is %d, should equal %d (number of observations)", 
                NROW(offset), ny), domain = NA)
    }
    if (is.empty.model(mt)) {
        x <- NULL
        z <- list(coefficients = if (mlm) matrix(NA_real_, 0, 
            ncol(y)) else numeric(), residuals = y, fitted.values = 0 * 
            y, weights = w, rank = 0L, df.residual = if (!is.null(w)) sum(w != 
            0) else ny)
        if (!is.null(offset)) {
            z$fitted.values <- offset
            z$residuals <- y - offset
        }
    }
    else {
        x <- model.matrix(mt, mf, contrasts)
        z <- if (is.null(w)) 
            lm.fit(x, y, offset = offset, singular.ok = singular.ok, 
                ...)
        else lm.wfit(x, y, w, offset = offset, singular.ok = singular.ok, 
            ...)
    }
    class(z) <- c(if (mlm) "mlm", "lm")
    z$na.action <- attr(mf, "na.action")
    z$offset <- offset
    z$contrasts <- attr(x, "contrasts")
    z$xlevels <- .getXlevels(mt, mf)
    z$call <- cl
    z$terms <- mt
    if (model) 
        z$model <- mf
    if (ret.x) 
        z$x <- x
    if (ret.y) 
        z$y <- y
    if (!qr) 
        z$qr <- NULL
    z
}
<bytecode: 0x5ce5e486c128>
<environment: namespace:stats>

Ce que nous voyons ci-dessus est le code sous-jacent de cette fonction particulière. Nous pourrions le copier et le coller dans notre propre script et y apporter toutes les modifications que nous jugerions nécessaires, mais en faisant preuve de prudence et en testant les changements apportés.

Ne vous inquiétez pas outre mesure si la majeure partie du code contenu dans les fonctions n’a pas de sens dans l’immédiat. C’est parfaitement normale, surtout si vous êtes novice en matière de R, auquel cas cela semble incroyablement intimidant. Honnêtement, cela peut être intimidant même après des années d’expérience avec R.

Pour remédier à cela, nous commencerons par créer nos propres fonctions en R dans la section suivante.

5.2 Fonctions en R

Les fonctions sont le pain et le beurre de R, ce sont les éléments essentiels qui vous permettent de travailler avec R.

Elles sont créées (la plupart du temps) avec le plus grand soin et la plus grande attention, mais peuvent finir par ressembler à un monstre de Frankenstein - avec des membres bizarrement attachés. Mais aussi alambiqués qu’elles puissent être, ils feront toujours fidèlement la même chose.

Cela signifie que les fonctions peuvent également être très stupides.

Si nous vous demandons d’aller au supermarché pour acheter les ingrédients nécessaires pour faire un poulet Balmoral même que vous ne savez pas ce que c’est, vous serez capable de deviner et prendre au moins quelque articles (du poulet par exemple). Ou, vous pouvez aussi décider de faire autre chose. Ou vous pouvez demander de l’aide à un chef cuisinier. Ou vous pouvez sortir votre téléphone et chercher sur internet, qu’est ce qu’un Poulet Balmoral ? Le fait est que, même si nous ne vous avons pas donné suffisamment d’informations pour accomplir la tâche, vous êtes suffisamment intelligent pour, au moins, essayer de trouver une solution.

Si, à la place, nous demandions à une fonction de faire la même chose, elle écouterait attentivement notre demande, puis renverrait simplement une erreur. Elle répéterait cela à chaque fois que nous lui demanderions de faire le travail lorsque la tâche n’est pas claire. Ce qu’il faut retenir ici, c’est que le code et les fonctions ne peuvent pas trouver de solutions de contournement pour pallier des informations mal fournies, ce qui est une excellente chose. C’est à vous de lui dire très explicitement ce qu’elle doit faire, étape par étape.

N’oubliez pas deux choses : l’intelligence du code vient du codeur, pas de l’ordinateur, et les fonctions ont besoin d’instructions exactes pour fonctionner.

Pour éviter que les fonctions ne soient trop stupides, vous devez fournir les informations dont la fonction a besoin pour fonctionner. Comme pour l’exemple du poulet Balmoral si nous avions fourni une liste d’ingrédients à la fonction, tout se serait bien passé. C’est ce que nous appelons “remplir un argument”. La grande majorité des fonctions exigent de l’utilisateur qu’il remplisse au moins un argument.

Ceci est illustré dans le pseudocode ci-dessous. Lorsque l’on crée une fonction, on peut :

  • Spécifier les arguments que l’utilisateur doit remplir (p.e. arg1 et arg2)
  • Fournir des valeurs par défaut aux arguments (p.e. arg2 = TRUE)
  • Définir ce qu’il faut faire avec ces arguments (expression) :
ma_fonction <- function(arg1, arg2, ...) {
  expression
}

La première chose à noter est que nous avons utilisé la fonction function() pour créer une nouvelle fonction appelée ma_fonction.

Entre les parenthèses (), on spécifie les informations (i.e., arguments) dont la fonction a besoin pour fonctionner (autant ou aussi peu que nécessaire).

Ces arguments sont ensuite transmis à la partie expression de la fonction. L’expression peut être n’importe quelle commande R valide ou n’importe quel ensemble de commandes R et est généralement entre une paire d’accolades {}.

Une fois que vous avez exécuté le code ci-dessus, vous pouvez utiliser votre nouvelle fonction en tapant :

ma_fonction(arg1, arg2)

Prenons un exemple pour clarifier les choses.

Tout d’abord, on crée un jeu de données appelé repas où les colonnes lasagnes, stovies, poutine et tartiflette sont remplis avec 10 valeurs aléatoires tirées d’un sac (à l’aide de la fonction rnorm() pour tirer des valeurs aléatoires d’une distribution normale avec une moyenne de 0 et un écart type de 1).

Nous incluons également un “problème”, que nous devrons résoudre plus tard, en incluant 3 NA dans la variable poutine (en utilisant rep(NA, 3)).

repas <- data.frame(
  lasagnes = rnorm(10),
  stovies = rnorm(10),
  poutine = c(rep(NA, 3), rnorm(7)),
  tartiflette = rnorm(10)
)

Supposons que vous souhaitiez multiplier les valeurs des variables stovies et lasagnes pour créer un nouvel objet appelé stovies_lasagnes.

Nous pouvons le faire “à la main” :

stovies_lasagnes <- repas$stovies * repas$lasagnes

Si c’était tout ce que nous avions à faire, on pourrait s’arrêter là.

R fonctionne avec des vecteurs, de sorte qu’effectuer ce type d’opérations dans R est en fait beaucoup plus simple que dans d’autres langages de programmation, où ce type de code peut nécessiter des boucles (nous disons que R est un langage vectorisé). Une chose à garder à l’esprit pour plus tard est que faire ce genre d’opérations avec des boucles peut être beaucoup plus lent que la vectorisation.

Mais que se passe-t-il si nous voulons répéter cette multiplication plusieurs fois ?

Supposons que nous voulions multiplier les colonnes lasagnes et stovies, stovies et tartiflette et poutine et tartiflette. Dans ce cas, nous pouvons copier et coller le code en remplaçant les informations pertinentes.

lasagnes_stovies <- repas$lasagnes * repas$stovies
stovies_tartiflette <- repas$stovies * repas$stovies
poutine_tartiflette <- repas$poutine * repas$tartiflette

Bien que cette approche fonctionne, il est facile de faire des erreurs.

Et en effet, ici, nous avons “oublié” de modifier stovies en tartiflette dans la deuxième ligne de code lors du copier-coller. C’est là que l’écriture d’une fonction s’avère utile !

Si nous écrivions cela sous forme de fonction, il n’y aurait qu’une seule source d’erreur potentielle (dans la fonction elle-même) au lieu de nombreuses lignes de code copiées-collées.

Astuce

En règle générale, si nous devons faire la même chose (par copier-coller et modifier) 3 fois ou plus, nous créons une fonction pour le faire.

Dans cet exemple, nous avons utilisé un code assez trivial où il est peut-être difficile de faire une véritable erreur. Mais que se passerait-il si nous augmentions la complexité ?

repas$lasagnes * repas$stovies / repas$lasagnes + (repas$lasagnes * 10^(repas$stovies))
- repas$stovies - (repas$lasagnes * sqrt(repas$stovies + 10))

Imaginez maintenant que vous deviez copier-coller ce code trois fois, et que vous deviez à chaque fois modifier l’élément lasagnes et stovies (surtout si nous devions le faire plus de trois fois).

Ce que nous pourrions faire à la place, c’est généraliser notre code pour x et y au lieu de nommer des plats spécifiques. En procédant de la sorte, nous pourrions recycler le code x * y. Chaque fois que nous voulions regrouper plusieurs colonnes, nous assignions un plat à x ou y.

Nous attribuerons la multiplication aux objets lasagnes_stovies et stovies_poutine afin de pouvoir y revenir plus tard.

# Définir les valeurs x et y
x <- repas$lasagnes
y <- repas$stovies

# Utiliser le code de multiplication
lasagnes_stovies <- x * y

# Définir les nouvelles valeurs x et y
x <- repas$stovies
y <- repas$poutine

# Ré-utiliser le code de multiplication
stovies_poutine <- x * y

C’est essentiellement ce que fait une fonction.

Appelons notre nouvelle fonction col_multiplicateur() et définissons-la avec deux arguments, x et y.

Une fonction dans R renvoie simplement sa dernière valeur. Toutefois, il est possible de forcer la fonction à renvoyer une valeur antérieure si cela s’avère nécessaire. Pour ce faire, il suffit d’utiliser la fonction return()

Ce n’est pas strictement nécessaire dans cet exemple car R retournera automatiquement la valeur de la dernière ligne de code de notre fonction. Nous l’incluons ici pour l’expliciter.

col_multiplicateur <- function(x, y) {
  return(x * y)
}

Maintenant que nous avons défini notre fonction, nous pouvons l’utiliser, ou “l’appeler”.

Utilisons la fonction pour multiplier les colonnes lasagnes et stovies en assignant le résultat à un nouvel objet appelé lasagna_stovies_func

lasagnes_stovies_fonc <- col_multiplicateur(x = repas$lasagnes, y = repas$stovies)
lasagnes_stovies_fonc
 [1] -0.25786044  0.02649975  0.26655427  0.32281812  0.47270826 -0.03753780
 [7]  1.48922093 -0.06543413 -1.30306589 -0.07169954

Si on ne s’intéresse qu’à la multiplication de repas$lasagnes par repas$stovies ce serait exagéré de créer une fonction pour faire quelque chose une seule fois.

Cependant, l’avantage de créer une fonction est que nous avons maintenant cette fonction ajoutée à notre environnement et que nous pouvons l’utiliser aussi souvent que souhaité.

Nous disposons également du code pour créer la fonction, ce qui signifie que nous pouvons l’utiliser dans des projets entièrement nouveaux, réduisant ainsi la quantité de code à écrire (et à tester) à chaque fois.

Pour s’assurer que la fonction a fonctionné correctement, nous pouvons comparer la variable lasagnes_stovies avec notre nouvelle variable lasagnes_stovies_fonc à l’aide de la fonction identical().

La fonction identical() teste si deux objets sont exactement identiques et renvoie un TRUE ou FALSE.

Tapez ?identical dans la console pour en savoir plus sur cette fonction.

identical(lasagnes_stovies, lasagnes_stovies_fonc)
[1] TRUE

Et nous confirmons que la fonction a produit le même résultat que le calcul manuel. Nous vous recommandons de prendre l’habitude de vérifier que la fonction que vous avez créée fonctionne comme vous le pensez.

Utilisons maintenant notre col_multiplicateur() pour multiplier les colonnes stovies et poutine. Remarquez maintenant que l’argument x reçoit la valeur repas$stovieset y la valeur repas$poutine.

stovies_poutine_fonc <- col_multiplicateur(x = repas$stovies, y = repas$poutine)
stovies_poutine_fonc
 [1]          NA          NA          NA  0.66973877  0.17138815  0.02865373
 [7] -0.06123004  0.24031431 -1.38710591 -0.19824080

Jusqu’à présent, tout va bien.

Tout ce que nous avons fait, c’est envelopper le code x * y dans une fonction, où nous demandons à l’utilisateur de spécifier à quoi correspondent x et y.

L’utilisation de la fonction est un peu longue car nous devons retaper le nom du jeu de données pour chaque variable. Pour nous amuser un peu, nous pouvons modifier la fonction afin de spécifier le jeu de données en tant qu’argument et les noms des colonnes sans les mettre entre guillemets (comme dans le style tidyverse 📦).

col_multiplicateur <- function(donnees, x, y) {
  temp_var <- donnees %>%
    select({{ x }}, {{ y }}) %>%
    mutate(xy = prod(.)) %>%
    pull(xy)
}

Pour cette nouvelle version de la fonction, nous avons ajouté un paramètre donnees à la ligne 1.

À la ligne 3, nous sélectionnons les variables x et y fournies comme arguments.

À la ligne 4, nous créons le produit des 2 colonnes sélectionnées et

À la ligne 5, nous extrayons la colonne que nous venons de créer.

Nous supprimons également la fonction return() puisqu’elle n’était pas nécessaire

Notre fonction est maintenant compatible avec la fonction tuyau, ou “pipe” (soit en natif |> ou magrittr 📦 %>%). Toutefois, étant donné que la fonction utilise désormais le pipe de magrittr 📦 et dplyr 📦, il faut charger le paquet tidyverse 📦 pour qu’elle fonctionne.

library(tidyverse)
lasagnes_stovies_fonc <- col_multiplicateur(repas, lasagnes, stovies)
lasagnes_stovies_fonc <- repas |> col_multiplicateur(lasagnes, stovies)

Ajoutons maintenant un peu plus de complexité.

Si vous regardez la sortie de poutine_tartiflette certains des calculs ont produit des valeurs NA. Cela s’explique par le fait qu’il y a des NA dans poutine (incluses lorsque nous avons créé le jeu de données repas). Malgré ces NA, la fonction semble avoir fonctionné, mais elle ne nous a donné aucune indication quant à l’existence d’un problème. Dans ce cas, nous préférerions qu’elle nous avertisse que quelque chose ne va pas.

Comment pouvons-nous faire en sorte que la fonction nous informe lorsque des NA sont produites ? Voici une solution.

col_multiplicateur <- function(donnees, x, y) {
  temp_var <- donnees %>%
    select({{ x }}, {{ y }}) %>%
    mutate(xy = {
      .[1] * .[2]
    }) %>%
    pull(xy)
  if (any(is.na(temp_var))) {
    warning("La fonction a produit des NA")
    return(temp_var)
  } else {
    return(temp_var)
  }
}
stovies_poutine_fonc <- col_multiplicateur(repas, stovies, poutine)
Warning in col_multiplicateur(repas, stovies, poutine): La fonction a produit
des NA
lasagnes_stovies_fonc <- col_multiplicateur(repas, lasagnes, stovies)

Le cœur de notre fonction reste le même, mais nous avons maintenant six lignes de code supplémentaires (lignes 6 à 11).

Nous avons inclus des structures conditionnelles, if (lignes 6-8) et else (lignes 9-11), afin de tester si des NAont été produits et, si c’est le cas, nous affichons un message d’avertissement à l’intention de l’utilisateur.

La section suivante de ce chapitre explique le fonctionnement et l’utilisation de ces structures conditionnelles.

5.3 Structures conditionnelles

x * y n’applique aucune logique. Il prend simplement la valeur de x et la multiplie par la valeur de y. Les structures conditionnelles permettent d’injecter de la logique dans votre code.

La structure conditionnelle la plus couramment utilisée est if. Chaque fois que vous voyez un if lisez-le comme “Si X est VRAI, fait quelque chose”.

Inclure un else permet simplement d’étendre la logique à “Si X est VRAI, fait quelque chose, sinon fait autre chose”.

if et else vous permettent d’exécuter des sections de code, en fonction d’une condition qui est soit TRUE ou FALSE.

Le pseudo-code ci-dessous vous montre la forme générale.

  if (condition) {
  Code executed when condition is TRUE
  } else {
  Code executed when condition is FALSE
  }

Pour approfondir la question, nous pouvons utiliser une vieille blague de programmeur pour poser un problème.

Le partenaire d’un.e. programmeu.r.se dit : “S’il-te-plaît, va au magasin et achète une brique de lait, s’ils ont des œufs, prends-en 6”.

Le.la programmeu.r.se revient avec 6 briques de lait.

Lorsque le partenaire s’en aperçoit, il s’exclame : “Pourquoi diable as-tu acheté 6 briques de lait ?”

Le.la programmeu.r.se répond “Ils avaient des œufs”

Au risque d’expliquer une blague, l’énoncé conditionnel ici est de savoir si le magasin avait ou non des œufs. Si le codage est conforme à la demande initiale, le.la programmeu.r.se doit apporter 6 briques de lait si le magasin a des œufs (condition = VRAI), ou apporter 1 brique de lait s’il n’y a pas d’œufs (condition = FAUX).

Dans R, cela est codé comme suit :

oeufs <- TRUE # Est-ce qu'il y a des œufs au magasin

if (oeufs == TRUE) { # S'il y a des œufs
  n.lait <- 6 # Prend 6 briques de lait
} else { # S'il n'y a pas d'œufs
  n.lait <- 1 # Prend 1 brique de lait
}

Nous pouvons alors vérifier n.lait le nombre de briques de lait que le.la programmeu.r.se a ramenées.

n.lait
[1] 6

Et comme dans la blague, notre code R n’a pas compris que la condition était de déterminer s’il fallait ou non acheter des œufs, et non plus du lait (il s’agit en fait d’un exemple libre du schéma de Winograd conçu pour tester la condition d’intelligence d’une intelligence artificielle en fonction de sa capacité à raisonner sur le sens d’une phrase).

Nous pourrions coder exactement la même structure conditionnelle de blague œuf-lait à l’aide de la fonction ifelse().

oeufs <- TRUE
n.lait <- ifelse(oeufs == TRUE, yes = 6, no = 1)

ifelse() fait exactement la même chose que la version plus étoffée de tout à l’heure, mais elle est maintenant condensée en une seule ligne de code.

Elle présente l’avantage supplémentaire de travailler sur des vecteurs plutôt que sur des valeurs individuelles (nous y reviendrons plus tard lorsque nous introduirons les boucles). La logique est lue de la même manière : “S’il y a des oeufs, assignez une valeur de 6 à n.lait s’il n’y a pas d’oeufs, assigner la valeur 1 à n.lait”.

Nous pouvons vérifier à nouveau que la logique renvoie toujours 6 briques de lait :

n.lait
[1] 6

Actuellement, il faudrait copier-coller du code pour changer la présence ou l’absence d’œufs dans le magasin. Nous avons appris plus haut comment éviter de nombreux copier-coller en créant une fonction. Comme avec la simple fonction x * y de notre précédente fonction col_multiplicateur() les déclarations logiques ci-dessus sont simples à coder et se prêtent bien à la transformation en fonction.

Et si nous faisions justement cela et enveloppions cette déclaration logique dans une fonction ?

lait <- function(oeufs) {
  if (oeufs == TRUE) {
    6
  } else {
    1
  }
}

Nous avons créé une fonction appelée lait() dont le seul argument est oeufs. L’utilisateur de la fonction spécifie si les œufs sont soit TRUE ou FALSE et la fonction utilisera alors une structure conditionnelle pour déterminer le nombre de cartons de lait renvoyés.

Essayons rapidement :

lait(oeufs = TRUE)
[1] 6

Et la plaisanterie est maintenue.

Remarquez que, dans ce cas, nous avons spécifié que nous remplissons l’argument oeufs (oeufs = TRUE). Dans certaines fonctions, comme la nôtre ici, lorsqu’une fonction n’a qu’un seul argument, nous pouvons être paresseux et ne pas nommer l’argument que nous remplissons. En réalité, on considère généralement qu’il est préférable d’indiquer explicitement les arguments que l’on remplit afin d’éviter les erreurs potentielles.

OK, revenons à la fonction col_multiplicateur() que nous avons créée ci-dessus et expliquons comment nous avons utilisé des structures conditionnelles pour avertir l’utilisateur si des NA sont produites lorsque nous multiplions deux colonnes.

col_multiplicateur <- function(donnees, x, y) {
  temp_var <- donnees %>%
    select({{ x }}, {{ y }}) %>%
    mutate(xy = {
      .[1] * .[2]
    }) %>%
    pull(xy)
  if (any(is.na(temp_var))) {
    warning("La fonction a produit des NA")
    return(temp_var)
  } else {
    return(temp_var)
  }
}

Dans cette nouvelle version de la fonction, on utilise toujours x * y, mais cette fois nous avons assigné les valeurs de ce calcul à un vecteur temporaire appelé temp_var afin de pouvoir l’utiliser dans nos structures conditionnelles.

Notez que ce temp_var est locale à notre fonction et n’existera pas en dehors de la fonction en raison de ce que l’on appelle les règles de cadrage de R.

Nous utilisons ensuite un if pour déterminer si notre temp_var contient des NA valeurs. Pour ce faire, nous utilisons la fonction is.na() pour vérifier si chaque valeur de notre temp_var est un NA.

is.na() renvoie TRUE si la valeur est un NA et FALSE si la valeur n’est pas un NA.

Nous imbriquons ensuite le is.na(temp_var) à l’intérieur de la fonction any() pour vérifier si au moins une des valeurs retournées par is.na(temp_var) est TRUE. Si c’est le cas, any() renverra une valeur TRUE.

Ainsi, s’il existe des NA dans notre temp_var la condition pour le if() sera TRUE alors que s’il n’y a pas de NA, la condition sera FALSE.

Si la condition est TRUE la fonction warning() génère un message d’avertissement à l’intention de l’utilisateur et renvoie la valeur de la variable temp_var.

Si la condition est FALSE le code sous la condition else est exécuté, ce qui renvoie simplement la valeur temp_var .

Ainsi, si nous exécutons notre col_multiplicateur() sur les colonnes repas$stovies et repas$poutine (qui contient NAs), nous recevrons un message d’avertissement.

stovies_poutine_fonc <- col_multiplicateur(repas, stovies, poutine)
Warning in col_multiplicateur(repas, stovies, poutine): La fonction a produit
des NA

En revanche, si nous multiplions deux colonnes qui ne contiennent pas de NA nous ne recevons pas de message d’avertissement

lasagnes_stovies_fonc <- col_multiplicateur(repas, lasagnes, stovies)

5.4 Combinaison d’opérateurs logiques

Les fonctions que nous avons créées jusqu’à présent sont parfaitement adaptées à nos besoins, bien qu’elles soient assez simplistes. Essayons de créer une fonction un peu plus complexe.

Nous allons créer une fonction permettant de déterminer si la journée d’aujourd’hui sera bonne ou non en fonction de deux critères : le jour de la semaine (vendredi ou non) et est-ce que votre code fonctionne ou non (VRAI ou FAUX).

Pour ce faire, nous utiliserons les structures conditonnelles if et else. La complexité vient ici des if qui suivent immédiatement les else. Nous utiliserons ces instructions conditionnelles quatre fois pour obtenir toutes les combinaisons possibles, qu’il s’agisse d’un vendredi ou non, et que votre code fonctionne ou non.

Nous utilisons également la fonction cat() pour produire un texte formaté correctement.

bonne.journee <- function(code.fonctionne, jours) {
  if (code.fonctionne == TRUE && jours == "Vendredi") {
    cat(
  "MEILLEURE.
  JOURNÉE.
    DE TOUS LES TEMPS.
      Arrête-toi là tant que ça dure et va au bar !"
    )
  } else if (code.fonctionne == FALSE && jours == "Vendredi") {
    cat("Bon... Au moins c'est vendredi ! Apéro !")
  } else if (code.fonctionne == TRUE && jours != "Vendredi") {
    cat("
  On était si proche d'une bonne journée...
  Mais c'est pas vendredi"
   )
  } else if (code.fonctionne == FALSE && jours != "Vendredi") {
    cat("Le monde est contre moi.")
  }
}
bonne.journee(code.fonctionne = TRUE, jours = "Vendredi")
MEILLEURE.
  JOURNÉE.
    DE TOUS LES TEMPS.
      Arrête-toi là tant que ça dure et va au bar !
bonne.journee(FALSE, "Tuesday")
Le monde est contre moi.

Vous avez remarqué que nous n’avons jamais spécifié ce qu’il fallait faire si le jour n’était pas un "Vendredi" ? C’est parce que, pour cette fonction, la seule chose qui compte est de savoir si c’est un vendredi (==) ou non (!=).

Nous avons également utilisé des opérateurs logiques chaque fois que nous avons utilisé la structure if. Les opérateurs logiques sont la dernière pièce du puzzle des conditions logiques. Ils sont résumés dans le tableau ci-dessous. Les deux premiers sont des opérateurs logiques et les six derniers sont des opérateurs relationnels. Vous pouvez utiliser n’importe lequel de ces opérateurs lorsque vous créez vos propres fonctions (ou boucles).

Opérateur Description technique Ce que cela signifie Exemple d’application
&& ET logique Les deux conditions doivent être remplies if(cond1 == test && cond2 == test
|| OU logique L’une ou l’autre des conditions doit être remplies if(cond1 == test || cond2 == test
< Inférieur à X est inférieur à Y if(X < Y)
> Supérieur à X est supérieur à Y if(X > Y)
<= Inférieur ou égal à X est inférieur/égal à Y if(X <= Y)
>= Supérieur ou égal à X est supérieur/égal à Y if(X >= Y)
== Egal à X est égal à Y if(X == Y)
!= N’est pas égal à X n’est pas égal à Y if(X != Y)

5.5 Boucles

R est très performant dans l’exécution de tâches répétitives. Si nous voulons qu’un ensemble d’opérations soit répété plusieurs fois, nous utilisons ce que l’on appelle une boucle. Lorsque vous créez une boucle, R exécute les instructions qu’elle contient un certain nombre de fois ou jusqu’à ce qu’une condition donnée soit remplie. Il existe trois principaux types de boucles dans R : la boucle for (“pour”) la boucle while (“tant que”) et la boucle repeat (“répéter”).

Les boucles sont l’un des éléments de base de tous les langages de programmation (pas seulement R), et peuvent être un outil puissant (bien qu’à notre avis, elles soient utilisées beaucoup trop souvent lors de l’écriture de code R).

5.5.1 Boucle “For” (pour)

La structure de boucle la plus couramment utilisée lorsque vous souhaitez répéter une tâche un nombre défini de fois est la boucle for.

L’exemple le plus simple de boucle for est le suivant :

for (i in 1:5) {
  print(i)
}
[1] 1
[1] 2
[1] 3
[1] 4
[1] 5

Mais que fait réellement le code ? Il s’agit d’un morceau de code dynamique où un index i est remplacé itérativement par chaque valeur du vecteur 1:5.

Décomposons.

Parce que la première valeur de notre séquence (1:5) est 1 la boucle commence par remplacer i par 1 et exécute tout ce qui se trouve entre les accolades {}.

Les boucles utilisent conventionnellement i comme compteur (“i” pour “itération”), mais vous êtes libre d’utiliser ce que vous voulez, même le nom de votre animal de compagnie, cela n’a pas vraiment d’importance (sauf lorsque vous utilisez des boucles imbriquées, auquel cas les compteurs doivent être appelés différemment, comme SenorWhiskers et HerrFlufferkins).

Ainsi, si nous devions effectuer manuellement la première itération de la boucle :

i <- 1
print(i)
[1] 1

Une fois cette première itération terminée, la boucle for revient au début et remplace i par la valeur suivante dans notre séquence 1:5 (2 dans ce cas) :

i <- 2
print(i)
[1] 2

Ce processus est ensuite répété jusqu’à ce que la boucle atteigne la dernière valeur de la séquence (5 dans cet exemple), après quoi elle s’arrête.

Pour mieux comprendre le fonctionnement de ces boucles for et vous présenter une caractéristique importante des boucles en général, nous allons modifier notre compteur à l’intérieur de la boucle. Cela peut être utilisé, par exemple, pour parcourir un vecteur, mais en sélectionnant la ligne suivante (ou toute autre valeur).

Pour ce faire, nous ajouterons simplement 1 à la valeur de notre index à chaque fois que nous itérons notre boucle.

for (i in 1:5) {
  print(i + 1)
}
[1] 2
[1] 3
[1] 4
[1] 5
[1] 6

Comme dans la boucle précédente, la première valeur de notre séquence est 1. La boucle commence par remplacer i par 1 mais cette fois, nous + 1 à chaque valeur de i dans l’expression. Le résultat est donc 1 + 1.

i <- 1
i + 1
[1] 2

Comme précédemment, une fois l’itération terminée, la boucle passe à la valeur suivante de la séquence et remplace i par la valeur suivante (2 dans ce cas), de sorte que i + 1 devient 2 + 1.

i <- 2
i + 1
[1] 3

Et ainsi de suite. Nous pensons que vous avez l’idée ! Dans les faits, c’est tout ce que fait une boucle for, rien d’autre !

Bien que nous ayons utilisé une simple addition dans le corps de la boucle, vous pouvez également combiner des boucles avec des fonctions.

Revenons à notre jeu de données repas. Précédemment dans le chapitre, nous avons créé une fonction pour multiplier deux colonnes et l’avons utilisée pour créer les variables lasagnes_stovies, stovies_poutine, et poutine_tartiflette. Nous aurions pu utiliser une boucle pour cela !

Rappelons-nous à quoi ressemblent nos données et le code de la fonction col_multiplicateur().

repas <- data.frame(
  lasagnes = rnorm(10),
  stovies = rnorm(10),
  poutine = c(rep(NA, 3), rnorm(7)),
  tartiflette = rnorm(10)
)
col_multiplicateur <- function(donnees, x, y) {
  temp_var <- donnees %>%
    select({{ x }}, {{ y }}) %>%
    mutate(xy = {
      .[1] * .[2]
    }) %>%
    pull(xy)
  if (any(is.na(temp_var))) {
    warning("La fonction a produit des NA")
    return(temp_var)
  } else {
    return(temp_var)
  }
}

Nous allons d’abord créer une liste vide (vous vous souvenez de Section 3.2.3 ?) que nous appelons temp (pour temporaire) qui sera utilisée pour stocker les résultats des itérations de la fonction via la boucle for.

temp <- list()
for (i in 1:(ncol(repas) - 1)) {
  temp[[i]] <- col_multiplicateur(repas, x = colnames(repas)[i], y = colnames(repas)[i + 1])
}
Warning in col_multiplicateur(repas, x = colnames(repas)[i], y =
colnames(repas)[i + : La fonction a produit des NA
Warning in col_multiplicateur(repas, x = colnames(repas)[i], y =
colnames(repas)[i + : La fonction a produit des NA

Lorsque nous spécifions notre boucle for remarquez que nous avons soustrait 1 à ncol(repas). La boucle ncol() renvoie le nombre de colonnes dans notre jeu de données repas, donc 4. Ainsi, notre boucle s’exécute de i = 1 à i = 4 - 1 c’est-à-dire, i = 3.

Ainsi, lors de la première itération de la boucle, i prend la valeur 1. col_multiplicateur() multiplie repas[, 1] (lasagnes) par repas[, 1 + 1] (stovies) et le stocke en tant que temp[[1]], donc le premier élément de la liste temp.

À la deuxième itération de la boucle, i prend la valeur 2. col_multiplicateur() multiplie repas[, 2] (stovies) pas repas[, 2 + 1] (poutine) et le stocke en tant que temp[[2]], donc le deuxième élément de la liste temp.

À la troisième et dernière itération de la boucle, i prend la valeur 3. col_multiplicateur() multiplie repas[, 3] (poutine) par repas[, 3 + 1] (tartiflette) et le stocke en tant que temp[[3]], donc le troisième élément de la liste temp.

Encore une fois, il est bon de vérifier que nous obtenons quelque chose de sensé de notre boucle (rappelez-vous, vérifiez, vérifiez et vérifiez encore !).

Pour ce faire, nous pouvons utiliser la fonction identical() pour comparer les variables que nous avons créées “by hand” avec chaque itération de la boucle manuellement.

lasagnes_stovies_fonc <- col_multiplicateur(repas, lasagnes, stovies)
i <- 1
identical(
  col_multiplicateur(repas, colnames(repas)[i], colnames(repas)[i + 1]),
  lasagnes_stovies_fonc
)
[1] TRUE
stovies_poutine_fonc <- col_multiplicateur(repas, stovies, poutine)
Warning in col_multiplicateur(repas, stovies, poutine): La fonction a produit
des NA
i <- 2
identical(
  col_multiplicateur(repas, colnames(repas)[i], colnames(repas)[i + 1]),
  stovies_poutine_fonc
)
Warning in col_multiplicateur(repas, colnames(repas)[i], colnames(repas)[i + :
La fonction a produit des NA
[1] TRUE

Si vous arrivez à suivre les exemples ci-dessus, vous êtes prêts pour commencer à écrire vos propres boucles for.

Mais, il existe d’autres types de boucles.

5.5.2 Boucle “While” (tant que)

La boucle while est utilisée lorsque vous voulez faire tourner en boucle jusqu’à ce qu’une certaine condition logique spécifique soit remplie (contrairement à la boucle for qui parcourt toujours une séquence entière).

La structure de base d’une boucle while est la suivante :

while (logical_condition) {
  expression
}

Un exemple simple de boucle while est :

i <- 0
while (i <= 4) {
  i <- i + 1
  print(i)
}
[1] 1
[1] 2
[1] 3
[1] 4
[1] 5

Ici, la boucle continuera seulement à transmettre des valeurs au corps principal de la boucle (l’expression) que lorsque i est inférieur ou égal à 4 (spécifié à l’aide de l’attribut <= dans cet exemple). Une fois que i est supérieur à 4, la boucle s’arrête.

Il existe un autre type de boucle, très rarement utilisé : la boucle repeat. La boucle repeat n’a pas de contrôle conditionnel et peut donc continuer à itérer indéfiniment. Ce qui signifie qu’une pause (“break”), ou “stop here”, doit être codée dans la boucle. C’est intéressant de savoir que ça éxiste, mais pour l’instant ce n’est pas très utile de s’en préoccuper ; les boucles for et while devraient vous permettre de répondre à la plupart de vos besoins.

5.5.3 Quand utiliser une boucle ?

Les boucles sont assez couramment utilisées, bien que parfois un peu trop à notre avis. Des tâches équivalentes peuvent être effectuées avec des fonctions, qui sont souvent plus efficaces.

La question se pose donc de savoir quand faut-il utiliser une boucle ?

En général, les boucles sont implémentées de manière inefficace dans R et doivent être évitées lorsque de meilleures alternatives existent, en particulier lorsque vous travaillez avec de grands ensembles de données. Cependant, les boucles sont parfois le seul moyen d’obtenir le résultat souhaité.

Voici quelques exemples de cas où l’utilisation de boucles peut s’avérer appropriée :

  • Certaines simulations1
  • Relations récursives 2
  • Problèmes plus complexes 3
  • Boucles While 4

5.5.4 Si on n’utilise pas une boucle, alors quoi ?

En bref, utilisez la famille de fonctions apply ; apply(), lapply(), tapply(), sapply(), vapply() et mapply().

Les fonctions apply peuvent souvent accomplir les tâches de la plupart des boucles “maison”, parfois plus rapidement (bien que cela ne soit pas vraiment un problème pour la plupart des gens), mais surtout avec un risque d’erreur beaucoup plus faible.

Une stratégie à garder à l’esprit et qui peut s’avérer utile est la suivante : pour chaque boucle que vous faites, essayez de la refaire en utilisant une fonction apply (souvent lapply ou sapply fonctionneront). Si vous le pouvez, utilisez la version applicable.

Il n’y a rien de pire que de se rendre compte qu’il y avait une petite, minuscule, erreur apparemment insignifiante dans une boucle qui, des semaines, des mois ou des années plus tard, s’est transformée en un énorme bazar.

Nous recommandons vivement d’essayer d’utiliser les fonctions apply chaque fois que cela est possible.

lapply

La fonction de base sera souvent lapply(), du moins au début.

La façon dont les lapply() fonctionnent, et la raison pour laquelle elles constituent souvent une bonne alternative aux boucles for, est qu’elles passent en revue chaque élément d’une liste et effectuent une tâche (c’est-à-dire exécutent une fonction).

Elles présentent l’avantage supplémentaire de produire les résultats sous forme de liste, ce que vous devriez autrement coder vous-même dans une boucle.

Une fonction lapply() a la structure suivante :

lapply(X, FUN)

Ici X est le vecteur auquel nous voulons faire quelque chose. On écrit FUN pour décrire ce qualifié ce qu’on est en train de faire là (je plaisante !). C’est aussi l’abréviation de “function” (fonction).

Commençons par une démonstration simple : Utilisons la fonction lapply() pour créer une séquence de 1 à 5 et ajouter 1 à chaque observation (comme nous l’avons fait avec une boucle for) :

lapply(0:4, function(a) {
  a + 1
})
[[1]]
[1] 1

[[2]]
[1] 2

[[3]]
[1] 3

[[4]]
[1] 4

[[5]]
[1] 5

Remarquez que nous devons spécifier notre séquence en tant que 0:4 pour obtenir la sortie 1 ,2 ,3 ,4 , 5 puisque nous ajoutons 1 à chaque élément de la séquence. Voyez ce qui se passe si vous utilisez 1:5 à la place.

De manière équivalente, nous aurions pu définir la fonction d’abord, puis l’utiliser dans lapply()

fun_ajouter <- function(a) {
  a + 1
}
lapply(0:4, fun_ajouter)
[[1]]
[1] 1

[[2]]
[1] 2

[[3]]
[1] 3

[[4]]
[1] 4

[[5]]
[1] 5

Les sapply() fait la même chose que lapply() mais au lieu de stocker les résultats sous forme de liste, elle les stocke sous forme de vecteur.

sapply(0:4, function(a) {
  a + 1
})
[1] 1 2 3 4 5

Comme vous pouvez le voir, dans les deux cas, nous obtenons exactement les mêmes résultats que lorsque nous avons utilisé la boucle for.


  1. Par exemple le modèle de Ricker peut, en partie, être construit à l’aide de boucles.↩︎

  2. Une relation qui dépend de la valeur de la relation précédente - “pour comprendre la récursivité, il faut comprendre la récursivité”.↩︎

  3. Par exemple, depuis combien de temps le dernier blaireau a-t-il été vu sur le site \(j\), sachant qu’une martre des pins a été vue à l’heure \(t\) au même endroit \(j\) que le blaireau, lorsque la martre a été détectée au cours d’une période spécifique de 6 heures, mais excluant les blaireaux vus 30 minutes avant l’arrivée de la martre, répétée pour toutes les détections de martres.↩︎

  4. Continuez à sauter jusqu’à ce que vous ayez atteint la lune.↩︎