Partie 16 Structures de données

R propose de nombreuses structures de données différentes, et les extensions peuvent en implémenter de nouvelles. Cette section introduit trois structures parmi les plus utilisées : les vecteurs atomiques, les listes et les tableaux de données. Certaines ont déjà été abordées et utilisées précédemment, mais connaître leurs spécificités et savoir les manipuler est utile voire indispensable, notamment lorsqu’on veut créer ses propres fonctions.

16.1 Vecteurs atomiques

Les vecteurs atomiques sont des structures qui regroupent ensemble plusieurs éléments constitués d’une seule valeur, avec deux contraintes : ces valeurs doivent toutes être du même type. Les vecteurs atomiques ont déjà été introduits section 9.1.

16.1.1 Création d’un vecteur

On peut construire un vecteur manuellement avec la fonction c().

x <- c(1, 3, 8)

Si on souhaite générer un vecteur de valeurs entières successives, on peut utiliser l’opérateur : ou la fonction seq_len().

2:8
#> [1] 2 3 4 5 6 7 8
seq_len(5)
#> [1] 1 2 3 4 5

La fonction seq() permet de générer des séquences régulière plus complexes.

seq(0.5, 2.5, by = 0.5)
#> [1] 0.5 1.0 1.5 2.0 2.5
seq(0, 4, length.out = 6)
#> [1] 0.0 0.8 1.6 2.4 3.2 4.0

Une autre variante de seq(), nommée seq_along(), permet de générer un vecteur d’entiers correspondant à la longueur d’un objet passé en argument :

x <- c("Pomme", "Poire")
seq_along(x)
#> [1] 1 2
y <- runif(10)
seq_along(y)
#>  [1]  1  2  3  4  5  6  7  8  9 10

Enfin, la fonction rep() permet de répéter un élément ou un vecteur.

rep("Pomme", 6)
#> [1] "Pomme" "Pomme" "Pomme" "Pomme" "Pomme" "Pomme"
rep(1:4, 2)
#> [1] 1 2 3 4 1 2 3 4

Si on souhaite connaître le nombre d’éléments d’un vecteur, on peut utiliser la fonction length().

v <- rep(1:4, 2)
length(v)
#> [1] 8

Il peut parfois être utile de créer des vecteurs “vides”. Dans ce cas on peut les initialiser avec les fonctions vector(), character() ou numeric(). Par défaut ces fonctions renvoient un vecteur sans élément, mais on peut aussi leur indiquer en argument le nombre d’éléments souhaités (qui seront alors initialisés avec une valeur par défaut).

numeric()
#> numeric(0)
character(2)
#> [1] "" ""

16.1.2 Vecteurs nommés

Les éléments d’un vecteur peuvent être nommés. Ces noms peuvent êtré définis au moment de la création du vecteur.

x <- c(e1 = 1, e2 = 3, e3 = 8)
x
#> e1 e2 e3 
#>  1  3  8

On peut utiliser names() pour récupérer les noms des éléments d’un vecteur.

names(x)
#> [1] "e1" "e2" "e3"

On peut aussi utiliser names() pour créer ou modifier les noms d’un vecteur existant.

names(x) <- c("brouette", "moto", "igloo")
x
#> brouette     moto    igloo 
#>        1        3        8

16.1.3 Types de vecteurs

On peut déterminer le type d’un vecteur avec l’instruction typeof.

x <- c(1, 3, 8)
typeof(x)
#> [1] "double"
y <- c("foo", "bar", "baz")
typeof(y)
#> [1] "character"
z <- c(TRUE, FALSE, FALSE)
typeof(z)
#> [1] "logical"

Parmi les principaux types de données on notera30 :

  • les chaînes de caractères (character)
  • les nombres flottants (double)
  • les nombres entiers (integer)
  • les valeurs logiques (logical)

À noter que par défaut les nombres sont considérés comme des nombres flottants (des nombres décimaux avec une virgule) : pour les définir explicitement comme nombres entiers on peut leur ajouter le suffixe L.

x <- c(1L, 3L, 8L)
typeof(x)
#> [1] "integer"

On peut tester le type d’un vecteur avec les fonctions is.character, is.double, is.logical… Autre fonction utile, is.numeric teste si un vecteur est de type double ou integer.

x <- c(1, 3, 8)
is.numeric(x)
#> [1] TRUE
x > 2
#> [1] FALSE  TRUE  TRUE
is.logical(x > 2)
#> [1] TRUE
y <- c("foo", "bar", "baz")
is.character(y)
#> [1] TRUE

Petite spécificité, les facteurs (voir section 9.3.1) ne sont pas considérés par R comme des character, même s’ils comportent des chaînes de caractères. Pour tester si un vecteur est de type facteur, on utilise is.factor().

fac <- factor(c("rouge", "vert", "rouge"))
is.character(fac)
#> [1] FALSE
is.factor(fac)
#> [1] TRUE

Tous les éléments d’un vecteur doivent être du même type. Si ça n’est pas le cas, les éléments seront convertis au type le plus “général” présent dans le vecteur, sachant que les character sont plus généraux que les numeric, qui sont eux-mêmes plus généraux que les logical.

Dans l’exemple suivant, le nombre 1 est transformé en chaîne de caractère "1".

c(1, "foo")
#> [1] "1"   "foo"

Si on mélange nombres et valeurs logiques, les TRUE sont convertis en 1 et les FALSE en 0.

c(TRUE, 2, FALSE)
#> [1] 1 2 0

Si la valeur NA, comme on l’a vu, permet d’indiquer une valeur manquante (Not Available), il existe en réalité plusieurs types de NA, même si cette distinction est la plupart du temps transparente pour l’utilisateur. On a ainsi notamment des valeurs NA_integer_, NA_character_, NA_real_.

La conversion automatique d’un type en un autre est à l’origine d’un idiome courant en R. Quand on applique une fonction qui attend un vecteur de nombres à un vecteur de valeurs logiques, celles-ci sont automatiquement converties, les TRUE devenant 1 et les FALSE devenant 0. Du coup, si on applique sum() à un vecteur de valeurs logiques, le résultat est égal au nombre de valeurs TRUE.

sum(c(TRUE, FALSE, TRUE))
#> [1] 2

On peut donc appliquer sum() à un test, et on obtiendra le nombre de valeurs pour lesquelles le test est vrai.

x <- c(1, 5, 8, 12, 14)
sum(x > 10)
#> [1] 2

Ceci fournit un raccourci très pratique. Dans l’exemple suivant, on tire 1000 nombres au hasard entre 0 et 1 et on calcule le nombre de valeurs obtenues qui sont inférieures à 0.5.

x <- runif(1000)
sum(x < 0.5)
#> [1] 513

Autre raccourci moins utilisé, appliquer mean() au résultat d’un test donne la proportion de valeurs pour lesquelles le test est vrai.

x <- c(1, 5, 8, 12, 14)
mean(x > 10)
#> [1] 0.4
x <- runif(1000)
mean(x < 0.5)
#> [1] 0.522

On peut convertir un vecteur d’un type à un autre avec les fonctions as.character(), as.numeric() et as.logical(). Si une valeur ne peut pas être convertie, elle est remplacée par un NA, et R affiche un avertissement.

as.character(1:3)
#> [1] "1" "2" "3"
as.logical(c(0, 2, 4))
#> [1] FALSE  TRUE  TRUE
as.numeric(c("foo", "23"))
#> Warning: NAs introduits lors de la conversion automatique
#> [1] NA 23

16.1.4 Sélection d’éléments

On a vu section 9.1 que l’opérateur [] peut être utilisé pour sélectionner des éléments d’un vecteur. Cet opérateur peut comporter :

  • des nombres (qui sélectionnent par position)
  • des chaînes de caractères (qui sélectionnent par nom)
  • un test ou des valeurs logiques (qui sélectionnent les éléments correspondant à TRUE)
x <- c(e1 = 1, e2 = 2, e3 = 8, e4 = 12)
x[c(1, 4)]
#> e1 e4 
#>  1 12
x[c("e2", "e4")]
#> e2 e4 
#>  2 12
x[x < 10]
#> e1 e2 e3 
#>  1  2  8

Si on fournit à [] un ou plusieurs nombres négatifs, les valeurs correspondantes seront supprimées plutôt que sélectionnées.

x[-1]
#> e2 e3 e4 
#>  2  8 12
x[c(-2, -4)]
#> e1 e3 
#>  1  8

Si on souhaite afficher les premières ou dernières valeurs d’un vecteur, les fonctions head() et tail() peuvent être utiles.

head(x, 2)
#> e1 e2 
#>  1  2
tail(x, 1)
#> e4 
#> 12

16.1.5 Modification

Utilisé conjointement avec l’opérateur d’assignation <-, l’opérateur [] permet de remplacer des éléments.

x <- c(e1 = 1, e2 = 2, e3 = 8, e4 = 12)
x[1] <- -1000
x
#>    e1    e2    e3    e4 
#> -1000     2     8    12
x["e2"] <- 0
x
#>    e1    e2    e3    e4 
#> -1000     0     8    12
x[x > 10] <- NA
x
#>    e1    e2    e3    e4 
#> -1000     0     8    NA

Utilisé sans arguments, [] se contente de renvoyer le vecteur entier. Mais couplé à une assignation, il remplace chacun des éléments du vecteur plutôt que le vecteur lui-même.

x[] <- 3
x
#> e1 e2 e3 e4 
#>  3  3  3  3

16.2 Listes

Les listes sont une généralisation des vecteurs : elles regroupent également plusieurs éléments ensemble, mais ceux-ci peuvent être de n’importe quel type, y compris des objets complexes. Une liste peut donc contenir des vecteurs, des listes, des tableaux de données, des fonctions, des graphiques ggplot2 stockés dans un objet, etc.

16.2.1 Création

On construit une liste avec la fonction list.

list(1, "foo", c("Pomme", "Citron"))
#> [[1]]
#> [1] 1
#> 
#> [[2]]
#> [1] "foo"
#> 
#> [[3]]
#> [1] "Pomme"  "Citron"

L’affichage du contenu d’une liste dans la console diffère de celui d’un vecteur. Dans le cas d’une liste les éléments sont affichés les uns en dessous des autres, et séparés par leur indice numérique entre une paire de crochets. Dans l’affichage ci-dessus, il faut bien distinguer les [[1]], [[2]] et [[3]], qui correspondent au numéro de l’élément de la liste, et les [1] qui font partie de l’affichage du contenu de ces éléments.

Comme pour les vecteurs, on peut nommer les éléments à la création de la liste.

liste <- list(nombre = 1, char = "foo", vecteur = c("Pomme", "Citron"))
liste
#> $nombre
#> [1] 1
#> 
#> $char
#> [1] "foo"
#> 
#> $vecteur
#> [1] "Pomme"  "Citron"

Dans ce cas l’affichage de la liste dans la console montre ces noms plutôt que les indices numériques des éléments.

Comme pour les vecteurs atomiques, on peut utiliser names() pour afficher ou modifier les noms des éléments.

names(liste)
#> [1] "nombre"  "char"    "vecteur"

Quand la liste est plus complexe, l’affichage peut vite devenir illisible.

liste <- list(
    l2 = list(x = 1:10, y = c("Pomme", "Citron")),
    df = data.frame(v1 = 2:5, v2 = LETTERS[2:5]),
    y = runif(10)
)
liste
#> $l2
#> $l2$x
#>  [1]  1  2  3  4  5  6  7  8  9 10
#> 
#> $l2$y
#> [1] "Pomme"  "Citron"
#> 
#> 
#> $df
#>   v1 v2
#> 1  2  B
#> 2  3  C
#> 3  4  D
#> 4  5  E
#> 
#> $y
#>  [1] 0.69901501 0.09206802 0.12078778 0.72223653 0.09476776 0.38314043
#>  [7] 0.48006825 0.65483899 0.12235615 0.33985941

Dans ce cas la fonction str peut être utile pour afficher de manière plus compacte la structure de la liste. Dans cet exemple elle permet de voir un peu plus clairement que x et y sont des éléments d’une sous-liste l2.

str(liste)
#> List of 3
#>  $ l2:List of 2
#>   ..$ x: int [1:10] 1 2 3 4 5 6 7 8 9 10
#>   ..$ y: chr [1:2] "Pomme" "Citron"
#>  $ df:'data.frame':  4 obs. of  2 variables:
#>   ..$ v1: int [1:4] 2 3 4 5
#>   ..$ v2: chr [1:4] "B" "C" "D" "E"
#>  $ y : num [1:10] 0.699 0.0921 0.1208 0.7222 0.0948 ...

16.2.2 Ajout d’éléments

Attention, si on souhaite ajouter un nouvel élément à une liste, il ne faut pas utiliser à nouveau list(), car dans ce cas notre liste de départ est insérée comme une “sous-liste”.

liste <- list(e1 = 1:3, e2 = "Chihuhua")
liste2 <- list(liste, nouveau = 100)
str(liste2)
#> List of 2
#>  $        :List of 2
#>   ..$ e1: int [1:3] 1 2 3
#>   ..$ e2: chr "Chihuhua"
#>  $ nouveau: num 100

Il faut à la place utiliser c(), comme pour les vecteurs.

liste3 <- c(liste, nouveau = 100)
str(liste3)
#> List of 3
#>  $ e1     : int [1:3] 1 2 3
#>  $ e2     : chr "Chihuhua"
#>  $ nouveau: num 100

c() permet aussi de “concaténer” deux listes existantes en une seule.

liste1 <- list(a = 1, b = 2)
liste2 <- list(x = 3, y = 4)
c(liste1, liste2)
#> $a
#> [1] 1
#> 
#> $b
#> [1] 2
#> 
#> $x
#> [1] 3
#> 
#> $y
#> [1] 4

16.2.3 Sélection d’éléments

Il y a deux opérateurs différents qui permettent de sélectionner les éléments d’une liste : les crochets simples [] et les crochets doubles [[]]. La différence entre ces deux opérateurs est souvent source de confusion.

Partons de la liste suivante :

liste <- list(1:5, "foo", c("Pomme", "Citron"))
liste
#> [[1]]
#> [1] 1 2 3 4 5
#> 
#> [[2]]
#> [1] "foo"
#> 
#> [[3]]
#> [1] "Pomme"  "Citron"

Si on utilise les crochets simples pour sélectionner le premier élément de cette liste, on obtient le résultat suivant :

liste[1]
#> [[1]]
#> [1] 1 2 3 4 5

On notera que le résultat est une liste à un seul élément.

Si on utilise les crochets doubles :

liste[[1]]
#> [1] 1 2 3 4 5

On obtient cette fois-ci non pas une liste composée du premier élément, mais le contenu de ce premier élément.

La différence est importante, mais pas toujours facile à retenir. On peut utiliser deux petites astuces mnémotechniques :

  • si une liste est un train composé de plusieurs wagons, [1] retourne le premier wagon du train, tandis que [[1]] renvoie le contenu du premier wagon.
  • une alternative est de considérer que [[]] va chercher “plus profondément” que [].

Un autre point important est que si on passe plusieurs éléments à [[]], la sélection se fait d’une manière récursive peu intuitive et source d’erreurs. Il est donc conseillé de toujours utiliser [[]] avec un seul argument, et d’utiliser [] si on souhaite sélectionner plusieurs éléments d’une liste.

liste[c(1, 2)]
#> [[1]]
#> [1] 1 2 3 4 5
#> 
#> [[2]]
#> [1] "foo"

En résumé :

  • si on souhaite récupérer uniquement le contenu d’un élément d’une liste, on utilise [[]] avec un seul argument.
  • si on souhaite récupérer une nouvelle liste en sélectionnant des éléments de notre liste actuelle, on utilise [] avec un ou plusieurs arguments.

Comme pour les vecteurs, on peut utiliser des nombres négatifs avec [] pour exclure des éléments plutôt que les sélectionner, et on peut également utiliser les fonctions head() et tail().

Si la liste est nommée, on peut sélectionner des éléments par noms avec les deux opérateurs.

liste <- list(nombre = 1, char = "foo", vecteur = c("Pomme", "Citron"))
liste[c("nombre", "char")]
#> $nombre
#> [1] 1
#> 
#> $char
#> [1] "foo"
liste[["vecteur"]]
#> [1] "Pomme"  "Citron"

On peut aussi utiliser l’opérateur $, qui équivaut à [[]] :

liste$vecteur
#> [1] "Pomme"  "Citron"

16.2.4 Modification

Comme pour les vecteurs, on peut utiliser l’opérateur [] et l’opérateur d’assignation <- pour modifier des éléments d’une liste.

liste <- list(nombre = 1:5, char = "foo", vecteur = c("Pomme", "Citron"))
liste["nombre"] <- "first"
liste
#> $nombre
#> [1] "first"
#> 
#> $char
#> [1] "foo"
#> 
#> $vecteur
#> [1] "Pomme"  "Citron"
liste[c(1, 3)] <- 0
liste
#> $nombre
#> [1] 0
#> 
#> $char
#> [1] "foo"
#> 
#> $vecteur
#> [1] 0

Attention à ne pas utiliser les crochets doubles pour modifier des éléments d’une liste car ceux-ci peuvent avoir un comportement inattendu si on veut modifier plusieurs éléments d’un coup.

Enfin, on peut supprimer un ou plusieurs éléments d’une liste, en leur attribuant la valeur NULL31.

liste <- list(nombre = 1:5, char = "foo", vecteur = c("Pomme", "Citron"))
liste$char <- NULL
liste
#> $nombre
#> [1] 1 2 3 4 5
#> 
#> $vecteur
#> [1] "Pomme"  "Citron"

16.2.5 Utilisation

En tant que généralisation des vecteurs atomiques, les listes sont utiles dès qu’on souhaite regrouper des éléments complexes ou hétérogènes.

On les utilisera par exemple pour retourner plusieurs résultats depuis une fonction.

indicateurs <- function(x) {
    list(
        moyenne = mean(x),
        variance = var(x)
    )
}

x <- 1:10
res <- indicateurs(x)
res$moyenne
#> [1] 5.5
res$variance
#> [1] 9.166667

On utilise également les listes pour stocker des objets complexes et leur appliquer des fonctions. Ce fonctionnement sera abordé en détail dans la section 18, mais en guise de petit aperçu, l’exemple fictif suivant récupère les noms de tous les fichiers CSV du répertoire courant et les importe tous dans une liste à l’aide de purrr::map() et de read_csv().

files <- list.files(pattern = "*.csv")
dfs <- purrr::map(files, read_csv)

On pourra ensuite utiliser cette liste de tableaux pour leur appliquer des transformations ou les fusionner.

16.3 Tableaux de données (data frame et tibble)

On a déjà utilisé les tableaux de données à de nombreux reprises en manipulant des data frames ou des tibbles. Les seconds sont une variante des premiers, les différences entre les deux ayant été abordées section 6.4.

Un tableau de données est en réalité une liste nommée de vecteurs atomiques avec une contrainte spécifique : ces vecteurs doivent tous être de même longueur, ce qui garantit le format “tabulaire” des données.

16.3.1 Création

Un tableau de données est le plus souvent créé en important des données depuis un fichier au format CSV, tableur ou autre. On peut cependant créer un data frame manuellement via la fonction data.frame() :

df <- data.frame(
    fruit = c("Pomme", "Pomme", "Citron"),
    poids = c(154, 167, 92),
    couleur = c("vert", "vert", "jaune")
)

On peut aussi créer un tibble manuellement avec la fonction tibble(). La syntaxe est la même que celle de data.frame(), mais avec un comportement un peu différent : notamment, les noms comportant des espaces ou des caractères spéciaux sont conservés tels quels.

La fonction tribble() permet de créer un tibble manuellement avec une syntaxe “par ligne” qui peut être un peu plus lisible.

df_trib <- tribble(
    ~fruit,   ~poids, ~couleur,
    "Pomme",  154,    "vert",
    "Pomme",  167,    "vert",
    "Citron", 92,     "jaune"
)

On peut convertir un data frame en tibble avec la fonction as_tibble().

df_tib <- as_tibble(df)
df_tib
#> # A tibble: 3 × 3
#>   fruit  poids couleur
#>   <chr>  <dbl> <chr>  
#> 1 Pomme    154 vert   
#> 2 Pomme    167 vert   
#> 3 Citron    92 jaune

16.3.2 Noms de colonnes et de lignes

On peut lister et modifier les noms des colonnes d’un tableau avec les fonctions names() ou colnames() (qui sont équivalentes).

names(df)
#> [1] "fruit"   "poids"   "couleur"
colnames(df)
#> [1] "fruit"   "poids"   "couleur"

On peut attribuer des noms aux lignes d’un data frame à l’aide de la fonction rownames(). Attention cependant, les noms de ligne ne sont (volontairement) pas pris en charge par les tibbles.

rownames(df) <- c("fruit1", "fruit2", "fruit3")
rownames(df)
#> [1] "fruit1" "fruit2" "fruit3"
rownames(df_tib) <- c("fruit1", "fruit2", "fruit3")
#> Warning: Setting row names on a tibble is deprecated.

Si on souhaite conserver des noms de ligne en passant d’un data frame à un tibble, il faut les stocker dans une nouvelle colonne, soit en la créant manuellement soit avec la fonction rownames_to_column() (qui a l’avantage de placer la nouvelle colonne en première position du tableau).

rownames_to_column(df, "name")
#>     name  fruit poids couleur
#> 1 fruit1  Pomme   154    vert
#> 2 fruit2  Pomme   167    vert
#> 3 fruit3 Citron    92   jaune

16.3.3 Sélection de lignes et de colonnes

On a déjà vu dans les parties précédentes plusieurs manières de sélectionner des éléments dans un tableau de données.

Ainsi, on peut sélectionner une colonne via l’opérateur $.

df$fruit
#> [1] "Pomme"  "Pomme"  "Citron"

Comme un tableau de données est en réalité une liste de colonnes, on peut aussi utiliser l’opérateur [[]] pour sélectionner l’une de ses colonnes, par position ou par nom32.

df[["fruit"]]
#> [1] "Pomme"  "Pomme"  "Citron"
df[[2]]
#> [1] 154 167  92

On peut utiliser head() et tail() avec un tableau de données : dans ce cas ces fonctions retourneront les premières ou dernières lignes du tableau.

head(df, 2)
#>        fruit poids couleur
#> fruit1 Pomme   154    vert
#> fruit2 Pomme   167    vert
tail(df, 1)
#>         fruit poids couleur
#> fruit3 Citron    92   jaune

On peut également utiliser l’opérateur [,] pour sélectionner à la fois des lignes et des colonnes, en lui passant deux arguments séparés par une virgule : d’abord la sélection des lignes puis celle des colonnes. Dans les deux cas on peut sélectionner par position, nom ou condition. Si on laisse un argument vide, on sélectionne l’intégralité des lignes ou des colonnes.

# Lignes 1 et 3 et colonne "poids"
df[c(1, 3), "poids"]
#> [1] 154  92
# Toutes les lignes et colonnes "poids" et "fruit"
df[, c("poids", "fruit")]
#>        poids  fruit
#> fruit1   154  Pomme
#> fruit2   167  Pomme
#> fruit3    92 Citron
# Lignes pour lesquelles poids > 150, et toutes les colonnes
df[df$poids > 150, ]
#>        fruit poids couleur
#> fruit1 Pomme   154    vert
#> fruit2 Pomme   167    vert
library(stringr)
# Colonnes dont le nom contient un "o", et toutes les lignes
df[, str_detect(names(df), "o")]
#>        poids couleur
#> fruit1   154    vert
#> fruit2   167    vert
#> fruit3    92   jaune

Attention, le comportement de [,] est différent entre les tibbles et les data frame lorsqu’on ne sélectionne qu’une seule colonne. Dans le cas d’un data frame, le résultat est un vecteur, dans le cas d’un tibble le résultat est un tableau à une colonne.

df[, "fruit"]
#> [1] "Pomme"  "Pomme"  "Citron"
df_tib[, "fruit"]
#> # A tibble: 3 × 1
#>   fruit 
#>   <chr> 
#> 1 Pomme 
#> 2 Pomme 
#> 3 Citron

Cette différence peut parfois être source d’erreurs, notamment quand on développe une fonction qui prend un tableau de données en argument.

16.3.4 Modification

On peut utiliser [[]] et [,] avec l’opérateur d’assignation <- pour modifier tout ou partie d’un tableau de données.

# Création d'une nouvelle colonne poids_kg
df[["poids_kg"]] <- df$poids / 1000
df
#>         fruit poids couleur poids_kg
#> fruit1  Pomme   154    vert    0.154
#> fruit2  Pomme   167    vert    0.167
#> fruit3 Citron    92   jaune    0.092
# Remplacement de la valeur de la colonne "fruit" pour les lignes 
# pour lesquelles "fruit" vaut "Citron"
df[df$fruit == "Citron", "fruit"] <- "Agrume"
df
#>         fruit poids couleur poids_kg
#> fruit1  Pomme   154    vert    0.154
#> fruit2  Pomme   167    vert    0.167
#> fruit3 Agrume    92   jaune    0.092

Pour conclure, on peut noter que l’utilisation des opérateurs [[]] et [,] sur un tableau de données peut sembler redondante et moins pratique que l’utilisation des verbes de dplyr comme select() ou filter(). Ils peuvent cependant être utiles lorsqu’on souhaite éviter les complications liées à l’utilisation du tidyverse à l’intérieur de fonctions, comme indiqué section 19. Ils peuvent également être plus rapides, et il est important de les connaître car on les rencontrera très fréquemment dans du code R sur le Web ou dans des packages.

16.4 Ressources

L’ouvrage R for Data Science (en anglais), accessible en ligne, contient un chapitre sur les vecteurs atomiques et les listes, et un chapitre dédié aux tibbles.

Pour aller encore plus loin, l’ouvrage Advanced R (également en anglais) aborde de manière approfondie les structures de données et les opérateurs de sélection [], [[]] et $.

16.5 Exercices

16.5.1 Vecteurs atomiques

Exercice 1.1

À l’aide de seq(), créer un vecteur v contenant tous les nombres pairs entre 10 et 20.

v <- seq(10, 20, by = 2)

Sélectionner les 3 premières valeurs de v.

v[1:3]
head(v, 3)

Sélectionner toutes les valeurs de v strictement inférieures à 15.

v[v < 15]

Créer une fonction derniere() qui prend en paramètre un vecteur et retourne son dernier élément (la fonction doit pouvoir s’appliquer à n’importe quel vecteur, quelle que soit sa longueur).

derniere(v)
#> [1] 20
derniere <- function(v) {
    v[length(v)]
}

# Ou bien

derniere <- function(v) {
    tail(v, 1)
}

Créer une fonction sauf_derniere() qui prend en paramètre un vecteur et retourne ce vecteur sans son dernier élément.

sauf_derniere(v)
#> [1] 10 12 14 16 18
sauf_derniere <- function(v) {
    v[-length(v)]
}

# Ou bien

sauf_derniere <- function(v) {
    head(v, -1)
}

Exercice 1.2

Soit le vecteur vn suivant :

vn <- c(val1 = 10, val2 = 0, val3 = 14)

Sélectionner les valeurs nommées “val1” et “val3”.

vn[c("val1", "val3")]

Créer une fonction select_noms() qui prend en argument un vecteur v et un ou plusieurs noms, et retourne uniquement les éléments de v correspondant à ces noms.

select_noms(vn, c("val2", "val3"))
#> val2 val3 
#>    0   14
select_noms <- function(v, noms) {
    v[noms]
}

Facultatif : créer une fonction sauf_nom() qui prend en argument un vecteur v et un nom, et retourne tous les éléments de v sauf celui correspondant à ce nom.

sauf_nom(vn, "val2")
#> val1 val3 
#>   10   14
sauf_nom <- function(v, nom) {
    v[names(v) != nom]
}

Facultatif : comparer les résultats des deux instructions suivantes.

vn["val1"]
vn[["val1"]]

Exercice 1.3

Soit les vecteurs x et y suivants :

x <- c(1, NA, 3, 4, NA)
y <- c(10, 20, 30, 40, 50)

À l’aide de l’opérateur [], sélectionner uniquement les valeurs NA de x.

x[is.na(x)]

De la même manière, sélectionner les valeurs de y correspondant aux valeurs NA de x (c’est-à-dire les valeurs 20 et 50).

y[is.na(x)]

En utilisant les deux instructions précédentes et l’opérateur d’assignation <-, remplacer les valeurs manquantes de x par les valeurs correspondantes de y.

x[is.na(x)] <- y[is.na(x)]

Exercice 1.4

Créer une fonction problemes_conversion qui :

  • prend en argument un vecteur v
  • le convertit en vecteur numérique
  • retourne les valeurs de v qui n’ont pas été converties correctement, c’est-à-dire celles qui ne valaient pas NA dans v mais valent NA après la conversion.

Vérifier avec :

x <- c("igloo", "20", NA, "3.5", "4,8")
problemes_conversion(x)
#> Warning in problemes_conversion(x): NAs introduits lors de la conversion
#> automatique
#> [1] "igloo" "4,8"
problemes_conversion <- function(v) {
    conv <- as.numeric(v)
    v[!is.na(v) & is.na(conv)]
}

16.5.2 Listes

Exercice 2.1

Créer une liste liste ayant la structure suivante :

#> List of 3
#>  $ : num 1
#>  $ : chr "oui"
#>  $ : int [1:3] 10 11 12
liste <- list(1, "oui", 10:12)

Donner les noms suivants aux éléments de la liste : num, reponse et vec.

names(liste) <- c("num", "reponse", "vec")

Ajouter un élément nommé chat et ayant pour valeur “Ronron” à la fin de liste.

liste <- c(liste, chat = "Ronron")

Modifier l’élément chat pour lui donner la valeur “Ronpchi”.

liste$chat <- "Ronpchi"
# Ou bien
liste["chat"] <- "Ronpchi"

Supprimer l’élément vec de liste.

liste$vec <- NULL
# Ou bien
liste["vec"] <- NULL

Exercice 2.2

Créer une fonction nommée extremes qui prend en argument un vecteur et retourne une liste nommée comportant sa valeur minimale et sa valeur maximale.

extremes <- function(x) {
    list(min = min(x), max = max(x))
}

Appliquer cette fonction à un vecteur de votre choix et utiliser le résultat pour calculer l’étendue (soit la différence entre la valeur maximale et la valeur minimale).

v <- runif(10)
res <- extremes(v)
res$max - res$min

Exercice 2.3

Soit la liste suivante :

liste <- list(1:3, runif(5), "youpi")

Sélectionner la sous liste composée des éléments 1 et 3 de liste.

liste[c(1, 3)]

Sélectionner la sous-liste composée du premier élément de liste.

liste[1]

Sélectionner le contenu du premier élément de liste.

liste[[1]]

En enchaînant deux opérations de sélection, sélectionner le deuxième élément du premier élément de liste.

liste[[1]][2]

Exercice 2.4

Créer une fonction description_liste qui prend en argument une liste et retourne :

  • son premier élément
  • son dernier élément
  • le nombre d’éléments qu’elle contient

Vérifier avec :

liste <- list(1:3, runif(5), "youpi")
description_liste(liste)
#> $premier_element
#> [1] 1 2 3
#> 
#> $dernier_element
#> [1] "youpi"
#> 
#> $nb_elements
#> [1] 3
description_liste <- function(liste) {
    list(
        premier_element = liste[[1]],
        dernier_element = liste[[length(liste)]],
        nb_elements = length(liste)
    )
}

16.5.3 Tableaux de données

Exercice 3.1

Créer le tableau df suivant :

df <- tribble(
    ~fruit,   ~poids, ~couleur,
    "Pomme",  154,    "vert",
    "Pomme",  167,    "vert",
    "Citron", 92,     "jaune"
)

À l’aide de l’opérateur $, sélectionner la colonne fruit de df.

df$fruit

Faire de même avec l’opérateur [[]].

df[["fruit"]]

À l’aide de l’opérateur [[]] et de la fonction str_to_upper() de stringr, transformer la colonne fruit en passant ses valeurs en majuscules.

library(stringr)
df[["fruit"]] <- str_to_upper(df[["fruit"]])

Créer une fonction colonne_maj qui prend en argument un tableau de données d et un nom de colonne colonne, et retourne le tableau avec la colonne correspondante convertie en majuscules. Vérifier avec :

colonne_maj(df, "couleur")
#> # A tibble: 3 × 3
#>   fruit  poids couleur
#>   <chr>  <dbl> <chr>  
#> 1 Pomme    154 VERT   
#> 2 Pomme    167 VERT   
#> 3 Citron    92 JAUNE
colonne_maj <- function(d, colonne) {
    d[[colonne]] <- str_to_upper(d[[colonne]])
    d
}

Exercice 3.2

Créer le tableau df suivant :

df <- tribble(
    ~fruit,   ~poids, ~couleur,
    "Pomme",  154,    "vert",
    "Pomme",  167,    "vert",
    "Citron", 92,     "jaune"
)

À l’aide de l’opérateur [,], sélectionner :

  • les citrons
  • les pommes et les colonnes fruit et couleur
  • la première colonne des lignes ayant un poids inférieur à 100
df[df$fruit == "Citron",]
df[df$fruit == "Pomme", c("fruit", "couleur")]
df[df$poids < 100, 1]

Créer une fonction filtre_valeur() qui prend un seul argument nommé valeur et retourne les lignes de df pour lesquelles la colonne fruit vaut valeur. Vérifier avec :

filtre_valeur("Pomme")
#> # A tibble: 2 × 3
#>   fruit poids couleur
#>   <chr> <dbl> <chr>  
#> 1 Pomme   154 vert   
#> 2 Pomme   167 vert
filtre_valeur <- function(valeur) {
    df[df$fruit == valeur,]
}

Modifier la fonction pour qu’elle accepte également un argument d contenant le tableau à filtrer. Vérifier avec :

filtre_valeur(df, "Pomme")
#> # A tibble: 2 × 3
#>   fruit poids couleur
#>   <chr> <dbl> <chr>  
#> 1 Pomme   154 vert   
#> 2 Pomme   167 vert
filtre_valeur <- function(d, valeur) {
    d[d$fruit == valeur,]
}

Modifier à nouveau la fonction pour qu’elle accepte aussi un argument colonne qui contient le nom de la colonne à utiliser pour filtrer les lignes. Vérifier avec :

filtre_valeur(df, colonne = "couleur", valeur = "jaune")
#> # A tibble: 1 × 3
#>   fruit  poids couleur
#>   <chr>  <dbl> <chr>  
#> 1 Citron    92 jaune
filtre_valeur <- function(d, colonne, valeur) {
    d[d[colonne] == valeur,]
}

Vérifier que cette fonction marche aussi sur un autre jeu de données :

library(questionr)
data(hdv2003)
filtre_valeur(hdv2003, "sexe", "Femme")

Exercice 3.3

Reprendre le tableau df des exercices précédents :

df <- tribble(
    ~fruit,   ~poids, ~couleur,
    "Pomme",  154,    "vert",
    "Pomme",  167,    "vert",
    "Citron", 92,     "jaune"
)

À l’aide de l’opérateur [,], effectuer les opérations suivantes :

  • Créer une nouvelle colonne id avec les valeurs 1, 2, 3
  • Remplacer la valeur “jaune” de la variable couleur par “jaune citron”
  • Créer une nouvelle colonne poids_rec qui vaut “léger” si poids est inférieur à 100, et “lourd” sinon
df[, "id"] <- 1:3
df[df$couleur == "jaune", "couleur"] <- "jaune citron"

df[df$poids < 100, "poids_rec"] <- "léger"
df[df$poids >= 100, "poids_rec"] <- "lourd"
# Ou bien :
df[, "poids_rec"] <- ifelse(df$poids < 100, "léger", "lourd")

Facultatif : effectuer les mêmes opérations en utilisant les verbes de dplyr.

df <- df %>% mutate(id = 1:3)

df <- df %>%
    mutate(
        couleur = ifelse(couleur == "jaune", "jaune_citron", couleur)
    )
# Ou bien :
library(forcats)
df <- df %>%
    mutate(
        couleur = fct_recode(couleur, "jaune_citron" = "jaune")
    )

df <- df %>%
    mutate(
        poids_rec = ifelse(poids < 100, "léger", "lourd")
    )

  1. Il en existe d’autres, comme complex ou raw, mais qui sont moins fréquemment utilisés.↩︎

  2. Si on veut ajouter un élément NULL à une liste, il faut utiliser les crochets simples avec la syntaxe liste["foo"] <- list(NULL).↩︎

  3. Attention, comme pour les listes, à ne pas utiliser [[]] avec un argument de longueur supérieur à 1, car cela mène soit à des erreurs soit à des résultats contre-intuitifs.↩︎