15  dplyr avancé

L’extension dplyr a déjà été présentée Chapitre 10. On va voir ici comment aller un peu plus loin dans l’utilisation du package, notamment en utilisant nos propres fonctions et en appliquant des transformations à des ensembles de colonnes.

On commence par charger les extensions du tidyverse ainsi que les jeux de données hdv2003 et rp2018 de l’extension questionr.

library(tidyverse)
library(questionr)
data(hdv2003)
data(rp2018)

15.1 Appliquer ses propres fonctions

15.1.1 Exemple avec mutate

Soit le jeu de données fictif suivant, dont chaque ligne représente un individu pour lequel on dispose de sa PCS, celle de ses parents, son âge et celui de ses enfants.

df <- tribble(
    ~id, ~pcs, ~pcs_pere, ~pcs_mere, ~age, ~`age enf1`, ~`age enf2`, ~`age enf3`,
      1,  "5",       "5",       "6",   25,           2,          NA,          NA,
      2,  "3",       "3",       "2",   45,          12,           8,           2,
      3,  "4",       "2",       "5",   29,           7,          NA,          NA,
      4,  "2",       "1",       "4",   32,           6,           3,          NA,
      5,  "1",       "4",       "3",   65,          39,          36,          28,
      6,  "6",       "6",       "6",   51,          18,          12,          NA,
      7,  "5",       "4",       "6",   37,           8,           4,           1,
      8,  "3",       "3",       "1",   42,          16,          10,           5
)

df
#> # A tibble: 8 × 8
#>      id pcs   pcs_pere pcs_mere   age `age enf1` `age enf2` `age enf3`
#>   <dbl> <chr> <chr>    <chr>    <dbl>      <dbl>      <dbl>      <dbl>
#> 1     1 5     5        6           25          2         NA         NA
#> 2     2 3     3        2           45         12          8          2
#> 3     3 4     2        5           29          7         NA         NA
#> 4     4 2     1        4           32          6          3         NA
#> 5     5 1     4        3           65         39         36         28
#> 6     6 6     6        6           51         18         12         NA
#> 7     7 5     4        6           37          8          4          1
#> 8     8 3     3        1           42         16         10          5

Dans ce tableau les PCS sont indiquées sous forme de codes : il serait plus lisible de les avoir sous forme d’intitulés de catégorie socio-professionnelle. On a vu Section 9.3.2 qu’on peut effectuer ce recodage avec la fonction fct_recode() de l’extension forcats.

df %>%
    mutate(
        pcs = fct_recode(pcs,
            "Agriculteur" = "1",
            "Indépendant" = "2",
            "Cadre" = "3",
            "Intermédiaire" = "4",
            "Employé" = "5",
            "Ouvrier" = "6"
        )
    )

Plutôt que d’intégrer le code du recodage directement dans le mutate(), on peut l’extraire en créant une fonction.

recode_pcs <- function(v) {
    fct_recode(v,
        "Agriculteur" = "1",
        "Indépendant" = "2",
        "Cadre" = "3",
        "Intermédiaire" = "4",
        "Employé" = "5",
        "Ouvrier" = "6"
    )
}

On peut dès lors simplifier notre mutate en appelant notre nouvelle fonction.

df %>%
    mutate(pcs = recode_pcs(pcs))

Premier avantage : on gagne en lisibilité. On a déplacé le code d’une opération spécifique dans une fonction avec un nom “parlant”, ce qui permet de savoir facilement à quoi elle sert. Et on a simplifié notre mutate qui est désormais plus lisible parce qu’il fait apparaître la logique de nos opérations (on veut recoder les PCS) sans en inclure les détails.

Le deuxième avantage évident, comme pour toute fonction, est qu’on peut la réutiliser pour appliquer ce recodage à plusieurs variables. Ainsi, si on veut recoder de la même manière pcs et pcs_mere, il suffit de faire :

df %>%
    mutate(
        pcs = recode_pcs(pcs),
        pcs_mere = recode_pcs(pcs_mere)
    )

Le code est plus court, plus lisible, on évite les erreurs de copier/coller, et si on souhaite modifier le recodage on n’a à intervenir qu’à un seul endroit en modifiant notre fonction.

15.1.2 Exemple avec summarise

Autre exemple, cette fois sur le jeu de données rp2018. Imaginons qu’on souhaite calculer, pour chaque région, le pourcentage de communes dont le nom se termine par une série de caractères donnée : par exemple, le pourcentage de communes dont le nom se termine par “ac”.

Comme il ne s’agit pas forcément d’une question triviale, on va décomposer le problème et rappeler (comme vu Section 11.6) que la fonction str_detect() de l’extension stringr permet de détecter quels éléments d’un vecteur de chaînes de caractères correspondent à une expression régulière. Ainsi, si on veut détecter si un nom de commune (variable rp2018$commune) se termine par "ac", on utilisera :

str_detect(rp2018$commune, "ac$")
Note

Le symbole \$ dans l’expression régulière "ac$" représente la fin de la chaîne de caractères. Il permet de s’assurer qu’on ne détecte que les noms de communes se terminant par “ac” (comme “Figeac”), et pas ceux contenant “ac” à un autre endroit (comme “Arcachon”).

Si on veut compter le nombre de communes pour lesquelles on a détecté une terminaison en “ac”, on peut utiliser un idiome courant en R et appliquer la fonction sum() au résultat précédent : les TRUE du résultat du str_detect sont alors convertis en 1, les FALSE en 0, et le sum() renverra donc le nombre de TRUE.

sum(str_detect(rp2018$commune, "ac$"))
#> [1] 131

Si on souhaite convertir ce résultat en pourcentage, il faut qu’on divise par le nombre total de communes, et qu’on multiplie par 100.

sum(str_detect(rp2018$commune, "ac$")) / length(rp2018$commune) * 100
#> [1] 2.418313

On crée une fonction nommée prop_suffixe qui a pour objectif d’effectuer ce calcul. Elle prend en entrée deux arguments : un vecteur de chaînes de caractères et un suffixe à détecter, et retourne le pourcentage d’éléments du vecteur se terminant par le suffixe. On rajoute nous-même le “$” à la fin du suffixe en question pour faciliter l’usage de la fonction.

Le résultat final est le suivant :

prop_suffixe <- function(v, suffixe) {
    # On ajoute $ à la fin du suffixe pour capturer uniquement en fin de chaîne
    suffixe <- paste0(suffixe, "$")
    # Détection du suffixe
    nb_detect <- sum(str_detect(v, suffixe))
    # On retourne le pourcentage
    nb_detect / length(v) * 100
}

On peut utiliser notre fonction de la manière suivante :

prop_suffixe(rp2018$commune, "ac")
#> [1] 2.418313

On a donc dans notre jeu de données 2.42% de communes dont le nom se termine par “ac”1.

Si maintenant on souhaite calculer ce pourcentage pour toutes les régions françaises, il suffit d’appeler notre fonction dans un summarise :

rp2018 %>%
    group_by(region) %>%
    summarise(prop_ac = prop_suffixe(commune, "ac")) %>%
    arrange(desc(prop_ac))
#> # A tibble: 17 × 2
#>    region                     prop_ac
#>    <chr>                        <dbl>
#>  1 Nouvelle-Aquitaine          10.8  
#>  2 Occitanie                    4.39 
#>  3 Bretagne                     4.26 
#>  4 Auvergne-Rhône-Alpes         2.25 
#>  5 Pays de la Loire             2.20 
#>  6 Bourgogne-Franche-Comté      0.995
#>  7 Provence-Alpes-Côte d'Azur   0.581
#>  8 Normandie                    0.347
#>  9 Hauts-de-France              0.185
#> 10 Centre-Val de Loire          0    
#> 11 Corse                        0    
#> 12 Grand Est                    0    
#> 13 Guadeloupe                   0    
#> 14 Guyane                       0    
#> 15 La Réunion                   0    
#> 16 Martinique                   0    
#> 17 Île-de-France                0

L’avantage d’avoir créé une fonction pour effectuer cette opération et qu’on peut du coup très facilement faire le même calcul en faisant varier le suffixe recherché.

rp2018 %>%
    group_by(region) %>%
    summarise(prop_ac = prop_suffixe(commune, "ieu")) %>%
    arrange(desc(prop_ac))
#> # A tibble: 17 × 2
#>    region                     prop_ac
#>    <chr>                        <dbl>
#>  1 Auvergne-Rhône-Alpes         2.79 
#>  2 Occitanie                    0.763
#>  3 Bourgogne-Franche-Comté      0.498
#>  4 Pays de la Loire             0.489
#>  5 Normandie                    0.347
#>  6 Nouvelle-Aquitaine           0.187
#>  7 Bretagne                     0    
#>  8 Centre-Val de Loire          0    
#>  9 Corse                        0    
#> 10 Grand Est                    0    
#> 11 Guadeloupe                   0    
#> 12 Guyane                       0    
#> 13 Hauts-de-France              0    
#> 14 La Réunion                   0    
#> 15 Martinique                   0    
#> 16 Provence-Alpes-Côte d'Azur   0    
#> 17 Île-de-France                0

En créant une fonction plutôt qu’en mettant notre code directement dans le summarise on a un script plus lisible, plus facile à maintenir, et des fonctionnalités facilement réutilisables.

15.1.3 Exemple avec rename_with

On a vu Section 10.2.3 que dplyr propose la fonction rename() pour renommer des colonnes d’un tableau de données. On peut l’utiliser par exemple pour remplacer un espace par un _ dans le nom d’une variable de df.

df %>% rename("age_enf1" = "age enf1")

Supposons maintenant qu’on souhaite appliquer la même transformation à l’ensemble des variables de df. Une solution pour cela est d’utiliser la fonction rename_with(), toujours fournie par dplyr, qui prend en argument non pas une correspondance "nouveau nom" = "ancien nom" mais une fonction qui sera appliquée à l’ensemble des noms de colonnes.

Par exemple, si on souhaite convertir tous les noms de colonnes en majuscules, on peut passer comme argument la fonction str_to_upper() de stringr.

df %>% rename_with(str_to_upper)
#> # A tibble: 8 × 8
#>      ID PCS   PCS_PERE PCS_MERE   AGE `AGE ENF1` `AGE ENF2` `AGE ENF3`
#>   <dbl> <chr> <chr>    <chr>    <dbl>      <dbl>      <dbl>      <dbl>
#> 1     1 5     5        6           25          2         NA         NA
#> 2     2 3     3        2           45         12          8          2
#> 3     3 4     2        5           29          7         NA         NA
#> 4     4 2     1        4           32          6          3         NA
#> 5     5 1     4        3           65         39         36         28
#> 6     6 6     6        6           51         18         12         NA
#> 7     7 5     4        6           37          8          4          1
#> 8     8 3     3        1           42         16         10          5

Pour remplacer les espaces par des _, on va d’abord créer une fonction ad hoc qui utilise str_replace_all.

remplace_espaces <- function(v) {
    str_replace_all(v, " ", "_")
}

Dès lors, on peut appliquer cette fonction à l’ensemble de nos noms de variables :

df %>% rename_with(remplace_espaces)
#> # A tibble: 8 × 8
#>      id pcs   pcs_pere pcs_mere   age age_enf1 age_enf2 age_enf3
#>   <dbl> <chr> <chr>    <chr>    <dbl>    <dbl>    <dbl>    <dbl>
#> 1     1 5     5        6           25        2       NA       NA
#> 2     2 3     3        2           45       12        8        2
#> 3     3 4     2        5           29        7       NA       NA
#> 4     4 2     1        4           32        6        3       NA
#> 5     5 1     4        3           65       39       36       28
#> 6     6 6     6        6           51       18       12       NA
#> 7     7 5     4        6           37        8        4        1
#> 8     8 3     3        1           42       16       10        5

Certain.es lectrices et lecteurs attentives auront peut-être noté que le même résultat peut être obtenu en utilisant remplace_espaces() avec la fonction names().

names(df) <- remplace_espaces(names(df))

L’avantage de rename_with() c’est qu’elle peut s’intégrer dans un pipeline de dplyr, et, comme nous allons le voir un peu plus loin, permet si nécessaire de n’appliquer cette transformation qu’à certaines colonnes seulement.

15.2 across() : appliquer des fonctions à plusieurs colonnes

15.2.1 Appliquer une fonction à plusieurs colonnes

On a défini précédemment une fonction qui recode les modalités d’une variable PCS et on a vu comment appliquer ce recodage à deux variables de df.

recode_pcs <- function(v) {
    fct_recode(v,
        "Agriculteur" = "1",
        "Indépendant" = "2",
        "Cadre" = "3",
        "Intermédiaire" = "4",
        "Employé" = "5",
        "Ouvrier" = "6"
    )
}

df %>%
    mutate(
        pcs = recode_pcs(pcs),
        pcs_mere = recode_pcs(pcs_mere)
    )

Supposons qu’on souhaite appliquer ce recodage à toutes les variables PCS de notre tableau. On pourrait évidemment créer autant de lignes que nécessaires dans notre mutate, mais on peut aussi utiliser la fonction across() de dplyr, qui facilite justement ce type d’opérations.

across() prend deux arguments principaux :

  • la définition d’un ensemble de colonnes de notre tableau de données
  • une ou plusieurs fonctions à appliquer aux colonnes sélectionnées

Il existe de nombreuses manières de définir les colonnes qu’on souhaite transformer : celles-ci sont en fait les mêmes que celles offertes par des verbes de dplyr comme select().

Une première possibilité est d’utiliser c() en lui passant les noms des variables (on notera qu’on n’est pas obligés de mettre ces noms entre guillemets).

df %>%
    mutate(
        across(
            c(pcs, pcs_mere),
            recode_pcs
        )
    )
#> # A tibble: 8 × 8
#>      id pcs           pcs_pere pcs_mere     age `age enf1` `age enf2` `age enf3`
#>   <dbl> <fct>         <chr>    <fct>      <dbl>      <dbl>      <dbl>      <dbl>
#> 1     1 Employé       5        Ouvrier       25          2         NA         NA
#> 2     2 Cadre         3        Indépenda…    45         12          8          2
#> 3     3 Intermédiaire 2        Employé       29          7         NA         NA
#> 4     4 Indépendant   1        Intermédi…    32          6          3         NA
#> 5     5 Agriculteur   4        Cadre         65         39         36         28
#> 6     6 Ouvrier       6        Ouvrier       51         18         12         NA
#> 7     7 Employé       4        Ouvrier       37          8          4          1
#> 8     8 Cadre         3        Agriculte…    42         16         10          5

Une autre possibilité est d’utiliser :, qui permet de définir une plage de colonnes en lui indiquant la colonne de début et la colonne de fin. Ainsi dans l’exemple suivant notre recodage est appliqué à toutes les colonnes situées entre pcs et pcs_mere (incluses).

df %>%
    mutate(
        across(
            pcs:pcs_pere,
            recode_pcs
        )
    )
#> # A tibble: 8 × 8
#>      id pcs           pcs_pere   pcs_mere   age `age enf1` `age enf2` `age enf3`
#>   <dbl> <fct>         <fct>      <chr>    <dbl>      <dbl>      <dbl>      <dbl>
#> 1     1 Employé       Employé    6           25          2         NA         NA
#> 2     2 Cadre         Cadre      2           45         12          8          2
#> 3     3 Intermédiaire Indépenda… 5           29          7         NA         NA
#> 4     4 Indépendant   Agriculte… 4           32          6          3         NA
#> 5     5 Agriculteur   Intermédi… 3           65         39         36         28
#> 6     6 Ouvrier       Ouvrier    6           51         18         12         NA
#> 7     7 Employé       Intermédi… 6           37          8          4          1
#> 8     8 Cadre         Cadre      1           42         16         10          5

On peut aussi sélectionner les variables via leurs noms. On peut ainsi choisir les variables qui commencent par une certaine chaîne de caractères via la fonction starts_with(), celles qui se terminent ou qui contiennent certains caractères avec ends_with() et contains().

df %>%
    mutate(
        across(
            starts_with("pcs"),
            recode_pcs
        )
    )
#> # A tibble: 8 × 8
#>      id pcs           pcs_pere   pcs_mere   age `age enf1` `age enf2` `age enf3`
#>   <dbl> <fct>         <fct>      <fct>    <dbl>      <dbl>      <dbl>      <dbl>
#> 1     1 Employé       Employé    Ouvrier     25          2         NA         NA
#> 2     2 Cadre         Cadre      Indépen…    45         12          8          2
#> 3     3 Intermédiaire Indépenda… Employé     29          7         NA         NA
#> 4     4 Indépendant   Agriculte… Intermé…    32          6          3         NA
#> 5     5 Agriculteur   Intermédi… Cadre       65         39         36         28
#> 6     6 Ouvrier       Ouvrier    Ouvrier     51         18         12         NA
#> 7     7 Employé       Intermédi… Ouvrier     37          8          4          1
#> 8     8 Cadre         Cadre      Agricul…    42         16         10          5

across() fonctionne dans un mutate, mais aussi dans un summarise. Dans l’exemple suivant, on calcule la moyenne de toutes les variables qui contiennent “enf”.

df %>%
    summarise(
        across(
            contains("enf"),
            mean
        )
    )
#> # A tibble: 1 × 3
#>   `age enf1` `age enf2` `age enf3`
#>        <dbl>      <dbl>      <dbl>
#> 1       13.5         NA         NA

De manière similaire, la fonction num_range() permet de sélectionner des colonnes ayant un préfixe commun suivi d’un indicateur numérique, comme x1, x2… Par exemple la syntaxe suivante sélectionnerait toutes les colonnes de Q01 à Q12 :

across(num_range("Q", 1:12, width = 2))

On peut également sélectionner des colonnes via une condition avec la fonction where(). Celle-ci prend elle-même en argument une fonction qui doit renvoyer TRUE ou FALSE, et ne conserve que les colonnes qui correspondent à des TRUE.

Dans l’exemple suivant, on applique la fonction mean seulement aux colonnes de df pour lesquelles la fonction is.numeric renvoie TRUE.

df %>%
    summarise(
        across(
            where(is.numeric),
            mean
        )
    )
#> # A tibble: 1 × 5
#>      id   age `age enf1` `age enf2` `age enf3`
#>   <dbl> <dbl>      <dbl>      <dbl>      <dbl>
#> 1   4.5  40.8       13.5         NA         NA

Pour des conditions plus complexes, on doit parfois définir soi-même la fonction passée à where(). Dans l’exemple suivant on calcule la moyenne uniquement pour les variables de df qui sont numériques et n’ont pas de valeurs manquantes.

no_na <- function(v) {
    is.numeric(v) && sum(is.na(v)) == 0
}

df %>%
    summarise(
        across(
            where(no_na),
            mean
        )
    )
#> # A tibble: 1 × 3
#>      id   age `age enf1`
#>   <dbl> <dbl>      <dbl>
#> 1   4.5  40.8       13.5

Il est même possible, pour les cas les plus complexes, de combiner plusieurs sélections avec les opérateurs &, | et !. L’exemple suivant applique la fonction mean() à toutes les colonnes numériques de df, sauf à la colonne id.

df %>%
    summarise(
        across(
            where(is.numeric) & !id,
            mean
        )
    )
#> # A tibble: 1 × 4
#>     age `age enf1` `age enf2` `age enf3`
#>   <dbl>      <dbl>      <dbl>      <dbl>
#> 1  40.8       13.5         NA         NA

Enfin, la fonction spéciale everything() permet de sélectionner la totalité des colonnes d’un tableau. Dans l’exemple suivant, on applique n_distinct() pour afficher le nombre de valeurs distinctes de toutes les variables de df.

df %>%
    summarise(
        across(
            everything(),
            n_distinct
        )
    )
#> # A tibble: 1 × 8
#>      id   pcs pcs_pere pcs_mere   age `age enf1` `age enf2` `age enf3`
#>   <int> <int>    <int>    <int> <int>      <int>      <int>      <int>
#> 1     8     6        6        6     8          8          7          5

Ces différentes manières de sélectionner un ensemble de colonnes sont appelées tidy selection. Il y a encore d’autres possibilités de sélection, pour avoir un aperçu complet on pourra se référer à la page de documentation de la fonction select().

Note

Une erreur de syntaxe fréquente est de mettre la sélection des colonnes dans l’appel à across(), mais pas la fonction qu’on souhaite appliquer.

Ainsi le code suivant génèrera une erreur :

mutate(across(pcs:pcs_mere), recode_pcs)

Il faut bien penser à passer la fonction comme argument du across(), donc à l’intérieur de ses parenthèses.

mutate(across(pcs:pcs_mere, recode_pcs))

15.2.2 Passer des arguments supplémentaires à la fonction appliquée

Par défaut, si on passe des arguments supplémentaires à across(), ils seront automatiquement transmis comme arguments à la fonction appliquée.

Dans l’exemple vu précédemment, on appliquait mean() à toutes les variables d’âge de df. Or comme certaines colonnes ont des valeurs manquantes, leur résultat vaut NA.

df %>%
    summarise(
        across(
            starts_with("age"),
            mean
        )
    )

Si on préfère que mean() soit appelée avec l’argument na.rm = TRUE, on pourrait définir explicitement une fonction à part qui utilise cet argument :

mean_sans_na <- function(x) {
    max(x, na.rm = TRUE)
}

df %>%
    summarise(
        across(
            starts_with("age"),
            mean_sans_na
        )
    )

Mais on peut faire plus simple, car tout argument supplémentaire passé à across() est transmis directement à la fonction appelée. Il est donc possible de faire :

df %>%
    summarise(
        across(
            starts_with("age"),
            max,
            na.rm = TRUE
        )
    )
#> Warning: There was 1 warning in `summarise()`.
#> ℹ In argument: `across(starts_with("age"), max, na.rm = TRUE)`.
#> Caused by warning:
#> ! The `...` argument of `across()` is deprecated as of dplyr 1.1.0.
#> Supply arguments directly to `.fns` through an anonymous function instead.
#> 
#>   # Previously
#>   across(a:b, mean, na.rm = TRUE)
#> 
#>   # Now
#>   across(a:b, \(x) mean(x, na.rm = TRUE))
#> # A tibble: 1 × 4
#>     age `age enf1` `age enf2` `age enf3`
#>   <dbl>      <dbl>      <dbl>      <dbl>
#> 1    65         39         36         28

15.2.3 Noms des colonnes créées par un mutate

Par défaut, lorsqu’on utilise across() dans un mutate, les nouvelles colonnes portent le même nom que les colonnes d’origine, ce qui signifie que ces dernières sont “écrasées” par les nouvelles valeurs.

Ainsi dans l’exemple suivant, les valeurs d’origine des colonnes PCS ont été écrasées par le résultat du recodage.

df %>%
    mutate(
        across(
            starts_with("pcs"),
            recode_pcs
        )
    )

Si on préfère créer de nouvelles colonnes, on doit indiquer la manière de les nommer en utilisant l’argument .names de across(). Celui prend comme valeur une chaîne de caractère dans laquelle le motif {.col} sera remplacé par le nom de la colonne d’origine.

Ainsi, si on souhaite plutôt que les variables recodées soient stockées dans de nouvelles colonnes nommées avec le suffixe _rec, on peut utiliser :

df %>%
    mutate(
        across(
            starts_with("pcs"),
            recode_pcs,
            .names = "{.col}_rec"
        )
    )
#> # A tibble: 8 × 11
#>      id pcs   pcs_pere pcs_mere   age `age enf1` `age enf2` `age enf3` pcs_rec  
#>   <dbl> <chr> <chr>    <chr>    <dbl>      <dbl>      <dbl>      <dbl> <fct>    
#> 1     1 5     5        6           25          2         NA         NA Employé  
#> 2     2 3     3        2           45         12          8          2 Cadre    
#> 3     3 4     2        5           29          7         NA         NA Interméd…
#> 4     4 2     1        4           32          6          3         NA Indépend…
#> 5     5 1     4        3           65         39         36         28 Agricult…
#> 6     6 6     6        6           51         18         12         NA Ouvrier  
#> 7     7 5     4        6           37          8          4          1 Employé  
#> 8     8 3     3        1           42         16         10          5 Cadre    
#> # ℹ 2 more variables: pcs_pere_rec <fct>, pcs_mere_rec <fct>

15.2.4 Appliquer plusieurs fonctions à plusieurs colonnes

across() offre également la possibilité d’appliquer plusieurs fonctions à un ensemble de colonnes. Dans ce cas, plutôt que de lui passer une seule fonction comme deuxième argument, on lui passe une liste nommée de fonctions.

Le code suivant calcule le minimum et le maximum pour les variables d’âge de df.

df %>%
    summarise(
        across(
            starts_with("age"),
            list(minimum = min, maximum = max)
        )
    )
#> # A tibble: 1 × 8
#>   age_minimum age_maximum `age enf1_minimum` `age enf1_maximum`
#>         <dbl>       <dbl>              <dbl>              <dbl>
#> 1          25          65                  2                 39
#> # ℹ 4 more variables: `age enf2_minimum` <dbl>, `age enf2_maximum` <dbl>,
#> #   `age enf3_minimum` <dbl>, `age enf3_maximum` <dbl>

Par défaut les nouvelles variables sont nommées sous la forme {nom_variable}_{nom_fonction}, mais on peut personnaliser cette règle en ajoutant un argument .names à across(). Cet argument est une chaîne de caractères dans laquelle {.col} sera remplacé par le nom de la colonne courante, et {.fn} par le nom de la fonction.

df %>%
    summarise(
        across(
            starts_with("age"),
            list(minimum = min, maximum = max),
            .names = "{.fn}_{.col}"
        )
    )
#> # A tibble: 1 × 8
#>   minimum_age maximum_age `minimum_age enf1` `maximum_age enf1`
#>         <dbl>       <dbl>              <dbl>              <dbl>
#> 1          25          65                  2                 39
#> # ℹ 4 more variables: `minimum_age enf2` <dbl>, `maximum_age enf2` <dbl>,
#> #   `minimum_age enf3` <dbl>, `maximum_age enf3` <dbl>

15.2.5 Renommer plusieurs colonnes avec une fonction

On a vu précédemment qu’on peut utiliser rename_with() pour renommer les colonnes d’un tableau de données à l’aide d’une fonction.

remplace_espaces <- function(v) {
    str_replace_all(v, " ", "_")
}

df %>% rename_with(remplace_espaces)

Par défaut, rename_with() applique la fonction de renommage à l’ensemble des colonnes du tableau. Il est cependant possible de lui indiquer de ne renommer que certaines de ces colonnes. Pour cela, on peut lui ajouter un argument supplémentaire nommé .cols, dont la syntaxe est exactement la même que pour across() ou select().

Par exemple, le code suivant convertit en majuscule uniquement les noms des colonnes id et poids.

df %>%
    rename_with(str_to_upper, .cols = starts_with("pcs"))
#> # A tibble: 8 × 8
#>      id PCS   PCS_PERE PCS_MERE   age `age enf1` `age enf2` `age enf3`
#>   <dbl> <chr> <chr>    <chr>    <dbl>      <dbl>      <dbl>      <dbl>
#> 1     1 5     5        6           25          2         NA         NA
#> 2     2 3     3        2           45         12          8          2
#> 3     3 4     2        5           29          7         NA         NA
#> 4     4 2     1        4           32          6          3         NA
#> 5     5 1     4        3           65         39         36         28
#> 6     6 6     6        6           51         18         12         NA
#> 7     7 5     4        6           37          8          4          1
#> 8     8 3     3        1           42         16         10          5

Et le code suivant remplace les espaces par des _ uniquement pour les colonnes dont le nom contient “enf”.

df %>%
    rename_with(remplace_espaces, .cols = contains("enf"))
#> # A tibble: 8 × 8
#>      id pcs   pcs_pere pcs_mere   age age_enf1 age_enf2 age_enf3
#>   <dbl> <chr> <chr>    <chr>    <dbl>    <dbl>    <dbl>    <dbl>
#> 1     1 5     5        6           25        2       NA       NA
#> 2     2 3     3        2           45       12        8        2
#> 3     3 4     2        5           29        7       NA       NA
#> 4     4 2     1        4           32        6        3       NA
#> 5     5 1     4        3           65       39       36       28
#> 6     6 6     6        6           51       18       12       NA
#> 7     7 5     4        6           37        8        4        1
#> 8     8 3     3        1           42       16       10        5

15.3 Fonctions anonymes et syntaxes abrégées

Dans les sections précédentes, nous avons rencontré plusieurs fonctions, comme rename_with() ou across(), qui prennent une fonction en argument.

Par exemple, dans l’utilisation suivante de rename_with(), on avait créé une fonction remplace_espaces().

remplace_espaces <- function(v) {
    str_replace_all(v, " ", "_")
}

df %>% rename_with(remplace_espaces)

Le fait de créer une fonction à part pour une opération d’une seule ligne ne se justifie pas forcément, surtout si on n’utilise pas cette fonction ailleurs dans notre code. Dans, ce cas, on peut définir notre fonction directement dans l’appel à rename_with() en utilisant une fonction anonyme, déjà introduites Section 14.4.2.

df %>%
    rename_with(function(v) {
        str_replace_all(v, " ", "_")
    })

Cette notation est assez pratique et souvent utilisée pour les fonctions à usage unique, ne serait-ce que pour s’économiser le fait de devoir lui trouver un nom pertinent.

La syntaxe étant un peu lourde, il existe deux alternatives permettant une définition plus “compacte”.

  • La première alternative est propre aux packages du tidyverse (notamment dplyr et purrr), et ne fonctionnera pas pour les fonctions n’appartenant pas à ces packages. Il s’agit d’utiliser une syntaxe de type “formule” : le corps de la formule contient les instructions de la fonction, et les arguments sont nommés .x (ou .) s’il n’y en a qu’un, .x et .y s’il y en a deux, et ..1, ..2, etc. s’ils sont plus nombreux.
  • La deuxième alternative est une syntaxe apparue avec la version 4.1 de R, qui permet de remplacer function(...) par le raccourci \(...).

Ainsi les définitions suivantes sont équivalents :

# Fonctionne partout et tout le temps
function(v) { v + 2 }
# Fonctionne uniquement dans les fonctions du tidyverse
~ { .x + 2 }
# Fonctionne uniquement à partir de R 4.1
\(v) { v + 2 }

De même que les définitions suivantes :

function(v1, v2) {
    res <- v1 / v2
    round(res, 1)
}

~ {
    res <- .x / .y
    round(res, 1)
}

\(v1, v2) {
    res <- v1 / v2
    round(res, 1)
}

Quand la fonction anonyme est constituée d’une seule instruction, on peut supprimer les accolades dans sa définition.

function(x) x + 2
~ .x + 2
\(x) x + 2

On pourra du coup, si on le souhaite, utiliser ces syntaxes compactes dans notre rename_with() pour définir notre fonction anonyme.

df %>%
    rename_with(~ str_replace_all(.x, " ", "_") )

df %>%
    rename_with( \(x) str_replace_all(x, " ", "_") )

Cette syntaxe peut être utilisée partout où on peut passer une fonction comme argument et donc définir des fonctions anonymes. Dans cet exemple déjà vu précédemment, on passe la fonction no_na comme argument de where().

no_na <- function(v) {
    is.numeric(v) && sum(is.na(v)) == 0
}

df %>%
    summarise(
        across(
            where(no_na),
            mean
        )
    )

On peut donc remplacer la fonction no_na par une fonction anonyme définie directement dans le where().

df %>%
    summarise(
        across(
            where(function(v) { is.numeric(v) && sum(is.na(v)) == 0 }),
            mean
        )
    )

Et du coup utiliser une des deux syntaxes “compactes”.

df %>%
    summarise(
        across(
            where(~ is.numeric(.x) && sum(is.na(.x)) == 0),
            mean
        )
    )

df %>%
    summarise(
        across(
            where(\(v) is.numeric(v) && sum(is.na(v)) == 0),
            mean
        )
    )

15.4 rowwise() et c_across() : appliquer une transformation ligne par ligne

Soit le tableau de données suivant, qui contient des évaluations de restaurants sur quatre critères différents2 :

restos <- tribble(
    ~nom,                       ~cuisine, ~decor, ~accueil, ~prix,
    "La bonne fourchette",             4,      2,        5,     4,
    "La choucroute de l'amer",         3,      3,        2,     3,
    "L'Hair de rien",                  1,      4,        4,     3,
    "La blanquette de Vaulx",          5,      4,        4,     5,
)

restos
#> # A tibble: 4 × 5
#>   nom                     cuisine decor accueil  prix
#>   <chr>                     <dbl> <dbl>   <dbl> <dbl>
#> 1 La bonne fourchette           4     2       5     4
#> 2 La choucroute de l'amer       3     3       2     3
#> 3 L'Hair de rien                1     4       4     3
#> 4 La blanquette de Vaulx        5     4       4     5

Imaginons qu’on souhaite faire la moyenne, pour chaque restaurant, des critères decor et accueil. On pourrait être tentés d’utiliser mean() de la manière suivante :

restos %>%
    mutate(
        decor_accueil = mean(c(decor, accueil))
    )
#> # A tibble: 4 × 6
#>   nom                     cuisine decor accueil  prix decor_accueil
#>   <chr>                     <dbl> <dbl>   <dbl> <dbl>         <dbl>
#> 1 La bonne fourchette           4     2       5     4           3.5
#> 2 La choucroute de l'amer       3     3       2     3           3.5
#> 3 L'Hair de rien                1     4       4     3           3.5
#> 4 La blanquette de Vaulx        5     4       4     5           3.5

Si on regarde le résultat, on constate qu’il ne correspond pas à ce que l’on souhaite puisque toutes les valeurs sont les mêmes.

Que s’est-il passé ? En fait le mutate s’est appliqué sur la totalité du tableau. Ceci signifie que dans mean(c(decor, accueil)), les objets decor et accueil correspondent à la totalité des valeurs de chaque variable. On a donc concaténé ces deux vecteurs et calculé la moyenne, qui est du coup la même pour chaque ligne.

La valeur obtenue correspond aux résultat de :

mean(c(restos$decor, restos$accueil))
#> [1] 3.5

Ce que nous souhaitons ici, c’est calculer la moyenne non pas pour l’ensemble du tableau mais pour chaque ligne. Pour cela, on va utiliser la fonction rowwise() : celle-ci est équivalente à un group_by() qui créerait autant de groupes qu’il y a de lignes dans notre tableau.

restos %>% rowwise()
#> # A tibble: 4 × 5
#> # Rowwise: 
#>   nom                     cuisine decor accueil  prix
#>   <chr>                     <dbl> <dbl>   <dbl> <dbl>
#> 1 La bonne fourchette           4     2       5     4
#> 2 La choucroute de l'amer       3     3       2     3
#> 3 L'Hair de rien                1     4       4     3
#> 4 La blanquette de Vaulx        5     4       4     5

Quant notre tableau est groupé via un rowwise(), les opérations s’effectuent sur un tableau constitué uniquement de la ligne courante. Si on calcule la moyenne précédente, on obtient désormais le bon résultat.

restos %>%
    rowwise() %>%
    mutate(decor_accueil = mean(c(decor, accueil)))
#> # A tibble: 4 × 6
#> # Rowwise: 
#>   nom                     cuisine decor accueil  prix decor_accueil
#>   <chr>                     <dbl> <dbl>   <dbl> <dbl>         <dbl>
#> 1 La bonne fourchette           4     2       5     4           3.5
#> 2 La choucroute de l'amer       3     3       2     3           2.5
#> 3 L'Hair de rien                1     4       4     3           4  
#> 4 La blanquette de Vaulx        5     4       4     5           4

Supposons qu’on souhaite désormais calculer la moyenne de l’ensemble des critères. On peut évidemment reprendre le code précédent en saisissant toutes les variables concernées.

restos %>%
    rowwise() %>%
    mutate(moyenne = mean(c(decor, accueil, cuisine, prix)))
#> # A tibble: 4 × 6
#> # Rowwise: 
#>   nom                     cuisine decor accueil  prix moyenne
#>   <chr>                     <dbl> <dbl>   <dbl> <dbl>   <dbl>
#> 1 La bonne fourchette           4     2       5     4    3.75
#> 2 La choucroute de l'amer       3     3       2     3    2.75
#> 3 L'Hair de rien                1     4       4     3    3   
#> 4 La blanquette de Vaulx        5     4       4     5    4.5

Lister les variables de cette manière peut vite devenir pénible si le nombre de variables est important. C’est pourquoi dplyr propose la fonction c_across() : celle-ci permet de sélectionner des colonnes de la même manière que select() ou across(), et retourne un vecteur constitué des valeurs concaténées de ces colonnes.

L’exemple suivant calcule la moyenne de toutes les colonnes comprises entre decor et prix, en utilisant l’opérateur :.

restos %>%
    rowwise() %>%
    mutate(
        moyenne = mean(c_across(decor:prix))
    )
#> # A tibble: 4 × 6
#> # Rowwise: 
#>   nom                     cuisine decor accueil  prix moyenne
#>   <chr>                     <dbl> <dbl>   <dbl> <dbl>   <dbl>
#> 1 La bonne fourchette           4     2       5     4    3.67
#> 2 La choucroute de l'amer       3     3       2     3    2.67
#> 3 L'Hair de rien                1     4       4     3    3.67
#> 4 La blanquette de Vaulx        5     4       4     5    4.33

Comme pour across() ou select(), on peut utiliser la fonction where() pour calculer la moyenne sur toutes les colonnes numériques.

restos %>%
    rowwise() %>%
    mutate(
        moyenne = mean(
            c_across(where(is.numeric))
        )
    )
#> # A tibble: 4 × 6
#> # Rowwise: 
#>   nom                     cuisine decor accueil  prix moyenne
#>   <chr>                     <dbl> <dbl>   <dbl> <dbl>   <dbl>
#> 1 La bonne fourchette           4     2       5     4    3.75
#> 2 La choucroute de l'amer       3     3       2     3    2.75
#> 3 L'Hair de rien                1     4       4     3    3   
#> 4 La blanquette de Vaulx        5     4       4     5    4.5

L’utilisation de rowwise() et c_across() est intéressante principalement quand il n’existe pas de fonction vectorisée pour la transformation qu’on souhaite appliquer. Quand elle existe, il est en général plus simple et plus rapide de l’utiliser.

Par exemple, pour trouver la valeur la plus élevée par restaurant, on pourrait être tenté d’utiliser le code suivant :

restos %>%
    rowwise() %>%
    summarise(note_max = max(c(decor, accueil)))
#> # A tibble: 4 × 1
#>   note_max
#>      <dbl>
#> 1        5
#> 2        3
#> 3        4
#> 4        4

Il est cependant plus lisible et plus efficace d’utiliser la fonction pmax, qui a justement pour objectif de parcourir des vecteurs en parallèle et de ne conserver que la plus grande valeur.

restos %>%
    summarise(note_max = pmax(decor, accueil))
#> Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
#> dplyr 1.1.0.
#> ℹ Please use `reframe()` instead.
#> ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
#>   always returns an ungrouped data frame and adjust accordingly.
#> # A tibble: 4 × 1
#>   note_max
#>      <dbl>
#> 1        5
#> 2        3
#> 3        4
#> 4        4

Une des limites de pmax cependant est qu’on ne peut pas l’utiliser avec c_across(), et qu’on ne peut donc pas faire de sélection des colonnes : on est obligés de saisir leurs noms.

restos %>%
    summarise(note_max = pmax(cuisine, decor, accueil, prix))
#> Warning: Returning more (or less) than 1 row per `summarise()` group was deprecated in
#> dplyr 1.1.0.
#> ℹ Please use `reframe()` instead.
#> ℹ When switching from `summarise()` to `reframe()`, remember that `reframe()`
#>   always returns an ungrouped data frame and adjust accordingly.
#> # A tibble: 4 × 1
#>   note_max
#>      <dbl>
#> 1        5
#> 2        3
#> 3        4
#> 4        5

Dans certains cas, notamment lorsque les colonnes sont nombreuses ou qu’on ne les a pas identifiées à l’avance, on pourra donc utiliser rowwise() et c_across() même quand des alternatives vectorisées existent.

restos %>%
    rowwise() %>%
    summarise(
        note_max = max(
            c_across(where(is.numeric))
        )
    )
#> # A tibble: 4 × 1
#>   note_max
#>      <dbl>
#> 1        5
#> 2        3
#> 3        4
#> 4        5

15.5 Ressources

La page d’aide de la fonction select (en anglais) liste toutes les possibilités offertes pour spécifier des ensembles de colonnes d’un tableau de données.

La vignette Column-wise operations de dplyr (en anglais) présente en détail l’utilisation et les fonctionnalités de across().

La vignette Row-wise operations de dplyr (toujours en anglais) présente de manière approfondie l’utilisation de rowwise() et c_across() pour opérer individuellement sur les lignes d’un tableau de données.

15.6 Exercices

Pour certains des exercices qui suivent on utilisera le jeu de données starwars de dplyr. On peut le charger avec les instructions suivantes :

library(dplyr)
data(starwars)

Le jeu de données contient les caractéristiques de 87 personnages présents dans les films : espèce, âge, planète d’origine, etc.

15.6.1 Appliquer ses propres fonctions

Exercice 1.1

Créer une fonction imc qui prend en argument un vecteur taille (en cm) et un vecteur poids (en kg) et retourne les valeurs correspondantes de l’indice de masse corporelle, qui se calcule en divisant le poids en kilos par la taille en mètres au carré.

imc <- function(tailles, poids) {
    tailles_m <- tailles / 100
    poids / tailles_m ^ 2
}

Utiliser cette fonction pour ajouter une nouvelle variable imc au tableau starwars.

starwars %>%
    mutate(imc = imc(height, mass))

À l’aide de group_by() et summarise(), utiliser à nouveau cette fonction pour calculer l’IMC moyen selon les valeurs de la variable species.

starwars %>%
    group_by(species) %>%
    summarise(
        imc = mean(imc(height, mass), na.rm = TRUE)
    )

Exercice 1.2

Toujours dans le jeu de données starwars, à l’aide d’un group_by() et d’un summarise(), calculer pour chaque valeur de la variable sex la valeur de l’étendue de la variable height du jeu de données starwars, c’est-à-dire la différence entre sa valeur maximale et sa valeur minimale.

starwars %>%
    group_by(sex) %>%
    summarise(
        etendue_taille = max(height, na.rm = TRUE) - min(height, na.rm = TRUE)
    )

En partant du code précédent, créer une fonction etendue qui prend en argument un vecteur et retourne la différence entre sa valeur maximale et sa valeur minimale. En utilisant cette fonction, calculer pour chaque valeur de sex la valeur de l’étendue des variables height et mass.

etendue <- function(v) {
    max(v, na.rm = TRUE) - min(v, na.rm = TRUE)
}
starwars %>%
    group_by(sex) %>%
    summarise(
        etendue_taille = etendue(height),
        etendue_poids  = etendue(mass)
    )

Exercice 1.3

On a vu que la fonction suivante permet de calculer le pourcentage des éléments d’un vecteur de chaînes de caractères se terminant par un suffixe passé en argument.

prop_suffixe <- function(v, suffixe) {
    # On ajoute $ à la fin du suffixe pour capturer uniquement en fin de chaîne
    suffixe <- paste0(suffixe, "$")
    # Détection du suffixe
    nb_detect <- sum(str_detect(v, suffixe))
    # On retourne le pourcentage
    nb_detect / length(v) * 100
}

Modifier cette fonction en une fonction prop_prefixe qui retourne le pourcentage d’éléments commençant par un préfixe passé en argument. Indication : pour détecter si une chaîne commence par "ker", on utilise l’expression régulière "^ker".

prop_prefixe <- function(v, prefixe) {
    # On ajoute $ à la fin du prefixe pour capturer uniquement en début de chaîne
    prefixe <- paste0("^", prefixe)
    # Détection du motif
    nb_detect <- sum(str_detect(v, prefixe))
    # On retourne le pourcentage
    nb_detect / length(v) * 100
}

Utiliser prop_prefixe dans un summarise appliqué à rp2018 pour calculer le pourcentage de communes commençant par “Saint” selon le département. Ordonner les résultats par pourcentage décroissant.

rp2018 %>%
    group_by(departement) %>%
    summarise(
        prop_saint = prop_prefixe(commune, "Saint")
    ) %>%
    arrange(desc(prop_saint))

Créer une fonction tab_prefixe qui prend un seul argument prefixe et renvoie le tableau obtenu à la question précédente pour le préfixe passé en argument. Tester avec tab_prefixe("Plou") et tab_prefixe("Sch")

tab_prefixe <- function(prefixe) {
    rp2018 %>%
        group_by(departement) %>%
        summarise(
            prop = prop_prefixe(commune, prefixe)
        ) %>%
        arrange(desc(prop))
}

Exercice 1.4

Le vecteur suivant donne, pour chacun des neuf principaux films de la saga Star Wars, la date à laquelle ils se déroulent dans l’univers de la saga.

c(
    "I"    = -32,
    "II"   = -22,
    "III"  = -19,
    "IV"   =   0,
    "V"    =   3,
    "VI"   =   4,
    "VII"  =  34,
    "VIII" =  34,
    "IX"   =  35
)

Dans le jeu de données starwars, la variable birth_year indique l’année de naissance du personnage en “années avant l’an zéro” (une valeur de 19 signifie donc une année de naissance de -19).

Créer une fonction age_film qui prend en entrée un vecteur d’années de naissance au même format que birth_year ainsi que l’identifiant d’un film, et calcule les âges à la date du film.

Vérifier avec :

age_film(starwars$birth_year, "IV")
#>  [1]  19.0 112.0  33.0  41.9  19.0  52.0  47.0    NA  24.0  57.0  41.9  64.0
#> [13] 200.0  29.0  44.0 600.0  21.0    NA 896.0  82.0  31.5  15.0  53.0  31.0
#> [25]  37.0  41.0  48.0    NA   8.0    NA  92.0    NA  91.0  52.0    NA    NA
#> [37]    NA    NA    NA  62.0  72.0  54.0    NA  48.0    NA    NA    NA  72.0
#> [49]  92.0    NA    NA    NA    NA    NA  22.0    NA    NA    NA  82.0    NA
#> [61]  58.0  40.0    NA 102.0  67.0  66.0    NA    NA    NA    NA    NA    NA
#> [73]    NA    NA    NA    NA    NA    NA    NA    NA    NA    NA    NA    NA
#> [85]    NA    NA  46.0
age_film <- function(annees, film) {
    annees_films <- c(
        "I"    = -32,
        "II"   = -22,
        "III"  = -19,
        "IV"   =   0,
        "V"    =   3,
        "VI"   =   4,
        "VII"  =  34,
        "VIII" =  34,
        "IX"   =  35
    )
    annees_naissance <- -annees
    annee_ref <- annees_films[film]
    annee_ref - annees_naissance
}

Utiliser la fonction pour ajouter deux nouvelles variables au tableau starwars : age_iv qui correspond à l’âge (potentiel) au moment du film IV, et age_ix qui correspond à l’âge au moment du film IX.

starwars %>%
    mutate(
        age_iv = age_film(birth_year, "IV"),
        age_ix = age_film(birth_year, "IX"),
    )

15.6.2 across()

Exercice 2.1

Reprendre la fonction etendue de l’exercice 1.2 :

etendue <- function(v) {
    max(v, na.rm = TRUE) - min(v, na.rm = TRUE)
}

Dans le jeu de données starwars, calculer l’étendue des variables height et mass pour chaque valeur de sex à l’aide de group_by(), summarise() et across().

starwars %>%
    group_by(sex) %>%
    summarise(
        across(
            c(height, mass),
            etendue
        )
    )

Toujours à l’aide d’across(), appliquer etendue à toutes les variables numériques, toujours pour chaque valeur de sex.

starwars %>%
    group_by(sex) %>%
    summarise(
        across(
            where(is.numeric),
            etendue
        )
    )

En utilisant & et !, appliquer etendue à toutes les variables numériques sauf à celles qui finissent par “year”.

starwars %>%
    group_by(sex) %>%
    summarise(
        across(
            where(is.numeric) & !ends_with("year"),
            etendue
        )
    )

Exercice 2.2

Dans le jeu de données starwars, appliquer en un seul summarise les fonctions min et max aux variables height et mass.

starwars %>%
    summarise(
        across(
            c(height, mass),
            list(min = min, max = max)
        )
    )

Si vous ne l’avez pas déjà fait à la question précédente, modifier le code pour que le calcul des valeurs minimales et maximales ne prennent pas en compte les valeurs manquantes.

funs <- list(
    min = function(v) { min(v, na.rm = TRUE) },
    max = function(v) { max(v, na.rm = TRUE) }
)
starwars %>%
    summarise(
        across(
            c(height, mass),
            funs
        )
    )
# Autre possibilité : les arguments supplémentaires passés à across() sont
# transmis aux fonctions appliquées
starwars %>%
    summarise(
        across(
            c(height, mass),
            list(min = min, max = max),
            na.rm = TRUE
        )
    )

Exercice 2.3

Dans le jeu de données hdv2003, utiliser across() pour transformer les modalités “Oui” et “Non” en TRUE et FALSE pour toutes les variables de hard.rock à sport.

detecte_oui <- function(v) {
    v == "Oui"
}
hdv2003 %>%
    mutate(
        across(
            hard.rock:sport,
            detecte_oui
        )
    )

Ajouter un argument .names à across() pour que les variables recodées soient stockées dans de nouvelles colonnes nommées avec le suffixe "_true".

detecte_oui <- function(v) {
    v == "Oui"
}
hdv2003 %>%
    mutate(
        across(
            hard.rock:sport,
            detecte_oui,
            .names = "{.col}_true"
        )
    )

15.6.3 Fonctions anonymes et notations abrégées

Exercice 3.1

Dans un exercice précédent, on a vu que le code ci-dessous permet de calculer l’étendue des variables height et mass du jeu de données starwars.

etendue <- function(v) {
    max(v, na.rm = TRUE) - min(v, na.rm = TRUE)
}

starwars %>%
    group_by(sex) %>%
    summarise(
        across(
            c(height, mass),
            etendue
        )
    )

Modifier ce code en supprimant la définition de etendue et en utilisant à la place une fonction anonyme directement dans le across().

starwars %>%
    group_by(sex) %>%
    summarise(
        across(
            c(height, mass),
            function(v) {
               max(v, na.rm = TRUE) - min(v, na.rm = TRUE)
            }
        )
    )

Modifier à nouveau ce code pour utiliser la syntaxe abrégée de type “formule” du tidyverse.

starwars %>%
    group_by(sex) %>%
    summarise(
        across(
            c(height, mass),
            ~ max(.x, na.rm = TRUE) - min(.x, na.rm = TRUE)
        )
    )

Exercice 3.2

Soit le code suivant, qui renomme les colonnes du tableau starwars de type liste en leur ajoutant le préfixe “liste_”.

ajoute_prefixe_liste <- function(nom) {
    paste0("liste_", nom)
}

starwars %>%
    rename_with(ajoute_prefixe_liste, .cols = where(is.list))

Réécrire ce code avec une fonction anonyme en utilisant les trois notations :

  • classique (avec function())
  • formule (du tidyverse)
  • compacte (à partir de R 4.1)
# Classique
starwars %>%
    rename_with(
        function(nom) { paste0("liste_", nom) },
        .cols = where(is.list)
    )
# Formule
starwars %>%
    rename_with(
        ~ paste0("liste_", .x),
        .cols = where(is.list)
    )
# Compacte
starwars %>%
    rename_with(
        \(nom) paste0("liste_", nom),
        .cols = where(is.list)
    )

Exercice 3.3

Le code suivant indique, pour chaque région du jeu de données rp2018, le nom de la commune ayant la valeur maximale pour les variables dipl_aucun et dipl_sup.

nom_commune_max <- function(valeurs, communes) {
    communes[valeurs == max(valeurs)]
}

rp2018 %>%
    group_by(region) %>%
    summarise(
        across(
            c(dipl_aucun, dipl_sup),
            nom_commune_max,
            commune
        )
    )
#> # A tibble: 17 × 3
#>    region                     dipl_aucun                   dipl_sup             
#>    <chr>                      <chr>                        <chr>                
#>  1 Auvergne-Rhône-Alpes       Oyonnax                      Corenc               
#>  2 Bourgogne-Franche-Comté    Saint-Loup-sur-Semouse       Fontaine-lès-Dijon   
#>  3 Bretagne                   Louvigné-du-Désert           Saint-Grégoire       
#>  4 Centre-Val de Loire        La Loupe                     Olivet               
#>  5 Corse                      Ghisonaccia                  Ville-di-Pietrabugno 
#>  6 Grand Est                  Behren-lès-Forbach           Mittelhausbergen     
#>  7 Guadeloupe                 Saint-Louis                  Le Gosier            
#>  8 Guyane                     Papaichton                   Remire-Montjoly      
#>  9 Hauts-de-France            Bohain-en-Vermandois         La Madeleine         
#> 10 La Réunion                 Cilaos                       La Possession        
#> 11 Martinique                 Basse-Pointe                 Schœlcher            
#> 12 Normandie                  Sourdeval                    Mont-Saint-Aignan    
#> 13 Nouvelle-Aquitaine         Aiguillon                    Bordeaux             
#> 14 Occitanie                  Bessèges                     Montferrier-sur-Lez  
#> 15 Pays de la Loire           Saint-Calais                 Nantes               
#> 16 Provence-Alpes-Côte d'Azur Marseille 15e Arrondissement Le Tholonet          
#> 17 Île-de-France              Clichy-sous-Bois             Paris 5e Arrondissem…

Réécrire ce code en utilisant une fonction anonyme, avec la syntaxe de votre choix (classique, formule ou compacte).

# Classique
rp2018 %>%
    group_by(region) %>%
    summarise(
        across(
            c(dipl_aucun, dipl_sup),
            function(valeurs, communes) { communes[valeurs == max(valeurs)] },
            commune
        )
    )
# Formule
rp2018 %>%
    group_by(region) %>%
    summarise(
        across(
            c(dipl_aucun, dipl_sup),
            ~ .y[.x == max(.x)],
            commune
        )
    )
# Compacte
rp2018 %>%
    group_by(region) %>%
    summarise(
        across(
            c(dipl_aucun, dipl_sup),
            \(valeurs, communes) communes[valeurs == max(valeurs)],
            commune
        )
    )

À l’aide d’une fonction anonyme supplémentaire, modifier le code pour qu’il retourne également, pour les mêmes variables, le nom des communes avec les valeurs minimales.

# Formule
rp2018 %>%
    group_by(region) %>%
    summarise(
        across(
            c(dipl_aucun, dipl_sup),
            list(
                max = ~ .y[.x == max(.x)],
                min = ~ .y[.x == min(.x)]
            ),
            commune
        )
    )
# Compacte
rp2018 %>%
    group_by(region) %>%
    summarise(
        across(
            c(dipl_aucun, dipl_sup),
            list(
                max = \(valeurs, communes) communes[valeurs == max(valeurs)],
                min = \(valeurs, communes) communes[valeurs == min(valeurs)]
            ),
            commune
        )
    ) %>% View()

15.6.4 rowwise() et c_across()

Exercice 4.1

On repart du code final de l’exercice 2.3, qui recodait une série de variables de hdv2003 en valeurs TRUE/FALSE dans de nouvelles variables avec le suffixe "_true".

detecte_oui <- function(v) {
    v == "Oui"
}
hdv2003 <- hdv2003 %>%
    mutate(
        across(
            hard.rock:sport,
            detecte_oui,
            .names = "{.col}_true"
        )
    )

Calculer le plus simplement possible une nouvelle variable total qui contient, pour chaque ligne, le nombre de valeurs TRUE des deux variables cinema_true et sport_true (si une ligne contient TRUE pour ces deux variables, total doit valoir 2, etc.)

hdv2003 %>%
    mutate(total = cuisine_true + sport_true)

Recalculer la variable total pour qu’elle contienne le nombre de TRUE par ligne pour les variables bricol_true, cinema_true et sport_true.

hdv2003 %>%
    rowwise() %>%
    mutate(total = sum(cuisine_true, sport_true, bricol_true))

Recalculer la variable total pour qu’elle contienne le nombre de TRUE par ligne pour toutes les variables se terminant par "_true".

hdv2003 %>%
    rowwise() %>%
    mutate(total = sum(c_across(ends_with("_true"))))

Reprendre le code précédent pour qu’il puisse s’appliquer directement sur les variables hard.rocksport, sans passer par le recodage en TRUE/FALSE.

count_oui <- function(v) {
    sum(v == "Oui")
}

hdv2003 %>%
    rowwise() %>%
    mutate(
        total = count_oui(c_across(hard.rock:sport))
    )

Exercice 4.2

Dans le jeu de données starwars, la colonne films contient la liste des films dans lesquels apparaissent les différents personnages. Cette colonne a une forme un peu particulière puisqu’il s’agit d’une “colonne-liste” : les éléments de cette colonne sont eux-mêmes des listes.

head(starwars$films, 3)
#> [[1]]
#> [1] "The Empire Strikes Back" "Revenge of the Sith"    
#> [3] "Return of the Jedi"      "A New Hope"             
#> [5] "The Force Awakens"      
#> 
#> [[2]]
#> [1] "The Empire Strikes Back" "Attack of the Clones"   
#> [3] "The Phantom Menace"      "Revenge of the Sith"    
#> [5] "Return of the Jedi"      "A New Hope"             
#> 
#> [[3]]
#> [1] "The Empire Strikes Back" "Attack of the Clones"   
#> [3] "The Phantom Menace"      "Revenge of the Sith"    
#> [5] "Return of the Jedi"      "A New Hope"             
#> [7] "The Force Awakens"

On essaye de calculer le nombre de films pour chaque personnage avec le code suivant. Est-ce que ça fonctionne ? Pourquoi ?

starwars %>%
    mutate(n_films = length(films))

Trouver une manière d’obtenir le résultat attendu.

starwars %>%
    rowwise() %>%
    mutate(n_films = length(films))

  1. Attention, le jeu de données ne comporte que les communes de plus de 2000 habitants.↩︎

  2. Un nom de salon de coiffure s’est glissé dans cette liste de restaurants. Saurez-vous le retrouver ?↩︎