Parcours
/
R
R
/
Programme
/
Data frames
Da

Data frames en R

1 exercice

À propos de Data frames

Dans d'autres parties du programme, on a vu plusieurs types de données aux caractéristiques différentes.

  • Les vecteurs atomiques sont à une dimension et de type homogène.
  • Les listes sont à une dimension et leurs éléments peuvent être de types hétérogènes.
  • Les matrices et tableaux sont multidimensionnels et homogènes.

Ce concept s'intéresse aux façons de stocker des données multidimensionnelles et hétérogènes. En pratique, la plupart des données du monde réel se présentent ainsi : on arrive donc maintenant au cœur de l'usage courant de R.

Les variantes de dataframe

Au fil des décennies, R a ajouté plusieurs types de données pour gérer des données tabulaires.

Dans ce programme, on se concentrera surtout sur les tibbles, mais il est utile de connaître quelques alternatives.

Le data.table

Les tibbles (décrits plus bas) et les data.tables sont deux tentatives reconnues pour améliorer le data.frame traditionnel du R de base. Les débats pour savoir lequel est « meilleur » sont inévitables. Il existe peut-être un certain consensus autour des points suivants (même si on les jugera simplistes).

  • tibble est optimisé principalement pour la facilité d'utilisation et l'intégration à l'écosystème Tidyverse.
  • data.table est optimisé principalement pour la puissance brute et la scalabilité, surtout lorsqu'on travaille avec de très grands jeux de données.

Quoi qu'il en soit, data.table n'est pas disponible sur Exercism, il n'est donc mentionné ici qu'à titre informatif.

Le data.frame

Dans le R de base, un data.frame est une list de vectors de même longueur. On peut le voir comme un tableau rectangulaire de données, dans lequel chaque colonne est homogène, mais chaque ligne peut (et contient d'ailleurs généralement) des types de données différents.

Un exemple pour illustrer cela :

# create the column vectors
languages <- c("Fortran", "R", "Python", "Julia")
created <- c(1957, 1993, 1991, 2012)
has.syllabus <- c(FALSE, TRUE, TRUE, TRUE)

# join columns to create the dataframe
df <- data.frame(languages, created, has.syllabus)
df
#>   languages created has.syllabus
#> 1   Fortran    1957        FALSE
#> 2         R    1993         TRUE
#> 3    Python    1991         TRUE
#> 4     Julia    2012         TRUE

# look at the structure
str(df)
#> 'data.frame':	4 obs. of  3 variables:
 #> $ languages   : chr  "Fortran" "R" "Python" "Julia"
 #> $ created     : num  1957 1993 1991 2012
 #> $ has.syllabus: logi  FALSE TRUE TRUE TRUE

On a une colonne de chaînes de caractères, une colonne de nombres et une colonne de booléens. À plus grande échelle, c'est une façon intuitive de représenter de nombreux ensembles de données du monde réel.

Le tibble

La conception du data.frame est ancienne.

Des décennies d'expérience, ainsi que l'évolution des usages de R, ont conduit à repenser le tout, donnant naissance à une alternative modernisée dans le Tidyverse : les tibbles.

Comparés au R de base, les tibbles offrent :

  • Des valeurs par défaut différentes, pour réduire les problèmes courants.
  • Une moindre propension à forcer la conversion des types de données à l'entrée.
  • Des messages d'erreur plus nombreux et plus clairs.
  • Des formats d'affichage différents, et généralement meilleurs.

En bref, un tibble vise à « en faire moins et râler plus », ce que l'on décrit aussi comme « paresseux et grognon ».

Ces types sont généralement interchangeables : toute fonction qui accepte un data.frame acceptera aussi un tibble, et vice versa.

La conversion entre les deux types est simple, avec as_tibble(df) et as.data.frame(tbl).

Pour du nouveau code, utiliser des tibbles t'aidera probablement à écrire du code plus robuste. Cependant, le code et les données hérités sont très répandus dans le monde de R, donc le data.frame a de bonnes chances de rester courant encore longtemps.

# column vectors are same as for data.frame
library(tibble)
tbl <- tibble(languages, created, has.syllabus)
tbl
  # A tibble: 4 × 3
#>   languages created has.syllabus
#>   <chr>       <dbl> <lgl>       
#> 1 Fortran      1957 FALSE       
#> 2 R            1993 TRUE        
#> 3 Python       1991 TRUE        
#> 4 Julia        2012 TRUE      
  
str(tbl)
#> tibble [4 × 3] (S3: tbl_df/tbl/data.frame)
#>  $ languages   : chr [1:4] "Fortran" "R" "Python" "Julia"
#>  $ created     : num [1:4] 1957 1993 1991 2012
#>  $ has.syllabus: logi [1:4] FALSE TRUE TRUE TRUE

Remarque le format d'affichage par défaut : la ligne de commentaire indiquant les dimensions est imprimée automatiquement, et les types des colonnes sont eux aussi affichés.

Travaille avec les tibbles

Les tibbles font partie intégrante du Tidyverse, on les charge donc avec library(tibble) ou library(tidyverse).

La documentation est assez complète, dans le style du Tidyverse :

Crée un tibble

Le plus simple est d'utiliser la fonction tibble() pour assembler des vecteurs colonnes, comme avec data.frame(). Un exemple de cette approche a été montré dans une section précédente.

S'il est plus pratique de saisir les valeurs ligne par ligne, la fonction correspondante est tribble().

tbl_r <- tribble(
  # column names marked with tilde prefix
  ~languages, ~created, ~has.syllabus,
  "Fortran", 1957, FALSE,
  "R", 1993, TRUE,
  "Python", 1991, TRUE,
  "Julia", 2012, TRUE
)

tbl_r
# A tibble: 4 × 3
#>   languages created has.syllabus
#>   <chr>       <dbl> <lgl>       
#> 1 Fortran      1957 FALSE       
#> 2 R            1993 TRUE        
#> 3 Python       1991 TRUE        
#> 4 Julia        2012 TRUE    

En pratique, il existe des dizaines de façons de créer des tibbles, car ils sont le format de sortie par défaut de toutes sortes de fonctions du Tidyverse. On y reviendra dans un prochain concept.

Manipule un tibble

Le concept de programmation fonctionnelle a présenté la bibliothèque purrr pour manipuler des vecteurs et des listes (des structures de données à une dimension).

Pour les dataframes (traditionnels ou tibbles), la bibliothèque à utiliser est dplyr.

On a déjà présenté dplyr plus haut, dans le concept Switch. Il n'y était question que de quelques fonctions utilitaires, mais on peut maintenant commencer à explorer le reste de cette vaste bibliothèque.

Extraire des sous-ensembles

Les dataframes, y compris les tibbles, peuvent être traités comme des listes de vecteurs colonnes, donc l'indexation de liste permet de récupérer une colonne donnée.

tbl
# A tibble: 4 × 3
#>   languages created has.syllabus
#>   <chr>       <dbl> <lgl>       
#> 1 Fortran      1957 FALSE
#> 2 R            1993 TRUE        
#> 3 Python       1991 TRUE        
#> 4 Julia        2012 TRUE    

tbl$created
#> [1] 1957 1993 1991 2012

Un dataframe peut aussi être indexé avec une indexation à la manière des matrices.

tbl[c(2, 4), 1:2]
  # A tibble: 2 × 2
#>   languages created
#>   <chr>       <dbl>
#> 1 R            1993
#> 2 Julia        2012

Dans le R moderne et son écosystème Tidyverse, les fonctions de dplyr sont généralement plus souples et plus pratiques, et c'est sur elles que portera le reste de ce concept.

Opérations par colonne

On récupère une seule colonne avec pull(), en indiquant son nom ou son numéro de position (un nombre négatif pour compter de la droite vers la gauche).

tbl |> pull(created)
#> [1] 1957 1993 1991 2012

Le résultat est le même que tbl$created, mais avec une fonction qui s'intègre bien dans un pipeline.

Pour récupérer plusieurs colonnes, la fonction adaptée est select(), qui est extrêmement polyvalente. On sélectionne (ou on écarte) des colonnes selon les propriétés de leur nom ou de leur type.

# Range with position and/or name
tbl |> select(1:created)
  # A tibble: 4 × 2
#>   languages created
#>   <chr>       <dbl>
#> 1 Fortran      1957
#> 2 R            1993
#> 3 Python       1991
#> 4 Julia        2012

# Exclude a column
tbl |> select(!created)
  # A tibble: 4 × 2
#>   languages has.syllabus
#>   <chr>     <lgl>       
#> 1 Fortran   FALSE       
#> 2 R         TRUE        
#> 3 Python    TRUE        
#> 4 Julia     TRUE        

# Use type of column
tbl |> select(where(is.numeric))
  # A tibble: 4 × 1
#>   created
#>     <dbl>
#> 1    1957
#> 2    1993
#> 3    1991
#> 4    2012

On peut combiner plusieurs critères à l'aide des opérateurs booléens &, | et ! (et, ou, non).

Les noms de colonnes qui sont des identifiants R valides n'ont pas besoin de guillemets dans un select(). Les noms invalides (par exemple ceux qui contiennent des espaces) peuvent être encadrés d'accents graves, même s'il vaut peut-être mieux les renommer.

La fonction select() peut s'appuyer sur toute une série de fonctions auxiliaires pour choisir des noms de colonnes : starts_with, contains, num_range et bien d'autres. matches permet une correspondance complète par expression régulière. Voir la documentation pour plus de détails.

Une telle puissance semble un peu ridicule avec notre dataframe d'exemple sur les langages. Heureusement, le tibble starwars est fourni avec dplyr, ce qui nous donne de quoi nous entraîner sur un jeu de données plus gros.

# limit display to top 3 rows of non-list columns
starwars |> 
  select(!where(is.list)) |> 
  head(3)
  # A tibble: 3 × 11
#>   name           height  mass hair_color skin_color  eye_color birth_year sex   gender    homeworld species
#>   <chr>           <int> <dbl> <chr>      <chr>       <chr>          <dbl> <chr> <chr>     <chr>     <chr>  
#> 1 Luke Skywalker    172    77 blond      fair        blue              19 male  masculine Tatooine  Human  
#> 2 C-3PO             167    75 NA         gold        yellow           112 none  masculine Tatooine  Droid  
#> 3 R2-D2              96    32 NA         white, blue red               33 none  masculine Naboo     Droid  

# pick a subset of columns
starwars |> 
  select(name | ends_with("color")) |> 
  head(5)
  # A tibble: 5 × 4
#>   name           hair_color skin_color  eye_color
#>   <chr>          <chr>      <chr>       <chr>    
#> 1 Luke Skywalker blond      fair        blue     
#> 2 C-3PO          NA         gold        yellow   
#> 3 R2-D2          NA         white, blue red      
#> 4 Darth Vader    none       white       yellow   
#> 5 Leia Organa    brown      light       brown    

Opérations par ligne

Note

De toute évidence, dplyr offre des moyens puissants de sélectionner des colonnes par leur nom.

Peut-on faire la même chose avec les noms de lignes ?

Non ! Les dataframes traditionnels de R peuvent avoir des noms de lignes, mais (après une longue histoire de bugs et de problèmes de performance) les noms de lignes ne sont pas autorisés dans les tibbles.

Si tu veux des noms, mets-les dans une colonne <chr> (en général la colonne 1), utilisée comme n'importe quelle autre colonne. Les fonctions d'import telles que as_tibble() les créent automatiquement lorsqu'on importe des données dont les lignes sont nommées.

Si cette limitation sur les noms de lignes te semble étrangement restrictive, souviens-toi que la plupart des grands systèmes de bases de données gèrent les tables de la même façon : Oracle, SQL Server, PostgreSQL, MySQL...

On récupère les lignes qui correspondent à certains critères avec filter(), ou on les exclut avec filter_out().

starwars |> 
  select(name:mass) |> 
  filter(between(height, 150, 165) & !is.na(mass))
  # A tibble: 4 × 3
#>   name               height  mass
#>   <chr>               <int> <dbl>
#> 1 Leia Organa           150    49
#> 2 Beru Whitesun Lars    165    75
#> 3 Nien Nunb             160    68
#> 4 Ben Quadinaros        163    65

Les critères de filtrage peuvent être aussi complexes que tu veux, mais ils reposent toujours sur le contenu des lignes.

Si l'on connaît les numéros des lignes, on peut utiliser diverses fonctions de la famille slice().

starwars |> 
  select(name | homeworld) |> 
  slice(20:25)
  # A tibble: 6 × 2
#>   name             homeworld
#>   <chr>            <chr>    
#> 1 Palpatine        Naboo    
#> 2 Boba Fett        Kamino   
#> 3 IG-88            NA       
#> 4 Bossk            Trandosha
#> 5 Lando Calrissian Socorro  
#> 6 Lobot            Bespin   

# random sample of rows
starwars |> 
  select(name | homeworld) |> 
  slice_sample(n = 4)
  # A tibble: 4 × 2
#>   name            homeworld
#>   <chr>           <chr>    
#> 1 Shaak Ti        Shili    
#> 2 Luminara Unduli Mirial   
#> 3 Grievous        Kalee    
#> 4 Palpatine       Naboo    

Pour supprimer les lignes en double, utilise distinct().

Modifie un tibble

Première mise en garde : le comportement par défaut de copie à la modification fait que le tibble d'origine reste généralement inchangé.

La plupart des modifications s'appliquent colonne par colonne.

On peut changer les noms de colonnes avec rename(newname = oldname), ou rename_with() pour appliquer une fonction. Un usage courant consiste à nettoyer les noms importés pour les rendre plus faciles à manipuler en R, en supprimant les espaces et en imposant un format cohérent pour les noms apparentés.

Remarque la syntaxe de rename(). Le contenu de la colonne oldname est lié au nom newname, d'où l'ordre.

L'ordre des colonnes peut être modifié avec relocate(). Par défaut, la ou les colonnes indiquées sont déplacées à la position la plus à gauche, mais on peut utiliser un argument .before ou .after pour un placement plus fin.

sw <- starwars |> select(name:species) |> slice(1:4)
sw
  # A tibble: 4 × 11
#>   name           height  mass hair_color skin_color  eye_color birth_year sex   gender    homeworld species
#>   <chr>           <int> <dbl> <chr>      <chr>       <chr>          <dbl> <chr> <chr>     <chr>     <chr>  
#> 1 Luke Skywalker    172    77 blond      fair        blue            19   male  masculine Tatooine  Human  
#> 2 C-3PO             167    75 NA         gold        yellow         112   none  masculine Tatooine  Droid  
#> 3 R2-D2              96    32 NA         white, blue red             33   none  masculine Naboo     Droid  
#> 4 Darth Vader       202   136 none       white       yellow          41.9 male  masculine Tatooine  Human  

sw |> relocate(c(species, homeworld), .after = name)
  # A tibble: 4 × 11
#>   name           species homeworld height  mass hair_color skin_color  eye_color birth_year sex   gender   
#>   <chr>          <chr>   <chr>      <int> <dbl> <chr>      <chr>       <chr>          <dbl> <chr> <chr>    
#> 1 Luke Skywalker Human   Tatooine     172    77 blond      fair        blue            19   male  masculine
#> 2 C-3PO          Droid   Tatooine     167    75 NA         gold        yellow         112   none  masculine
#> 3 R2-D2          Droid   Naboo         96    32 NA         white, blue red             33   none  masculine
#> 4 Darth Vader    Human   Tatooine     202   136 none       white       yellow          41.9 male  masculine

Pour des modifications plus importantes, mutate() te permet de :

  • Créer de nouvelles colonnes qui sont des fonctions de colonnes existantes.
  • Remplacer une colonne existante, en créant une nouvelle colonne du même nom.
  • Supprimer une colonne, en donnant à sa valeur la valeur NULL

De toute évidence, mutate() est puissant, potentiellement déroutant, et une bonne raison de se réjouir de la copie à la modification.

Il n'y a aucune raison évidente de s'intéresser à l'indice de masse corporelle des personnages de Star Wars, mais au cas où :

starwars |> 
  select(c(name, species, height, mass)) |> 
  mutate(BMI = mass / (height / 100)^2) |> 
  head(4)
  # A tibble: 4 × 5
#>   name           species height  mass   BMI
#>   <chr>          <chr>    <int> <dbl> <dbl>
#> 1 Luke Skywalker Human      172    77  26.0
#> 2 C-3PO          Droid      167    75  26.9
#> 3 R2-D2          Droid       96    32  34.7
#> 4 Darth Vader    Human      202   136  33.3

Quand tu veux agir sur un sous-ensemble de colonnes avec des fonctions comme mutate(), la séquence select() |> mutate() de l'exemple ci-dessus est une option. Seules les colonnes sélectionnées figureront dans le résultat.

Autrement, il peut être pratique d'utiliser pick() à l'intérieur de l'appel à mutate() :

starwars |> mutate(pick(c(name, species, height, mass)), BMI = mass / (height / 100)^2) |> head(4)
When you want to operate on a subset of the columns with functions such as `mutate()`, the `select() |> mutate()` sequence in the above example is one option.
Only the selected columns will be in the result.

Alternatively, it can be convenient to use `pick()` _within_ the `mutate()` call:

```R
starwars |> mutate(pick(c(name, species, height, mass)), BMI = mass / (height / 100)^2) |> head(4)
    # A tibble: 4 × 15
#>   name         height  mass hair_color skin_color eye_color birth_year sex   gender homeworld species films vehicles
#>   <chr>         <int> <dbl> <chr>      <chr>      <chr>          <dbl> <chr> <chr>  <chr>     <chr>   <lis> <list>  
#> 1 Luke Skywal…    172    77 blond      fair       blue            19   male  mascu… Tatooine  Human   <chr> <chr>   
#> 2 C-3PO           167    75 NA         gold       yellow         112   none  mascu… Tatooine  Droid   <chr> <chr>   
#> 3 R2-D2            96    32 NA         white, bl… red             33   none  mascu… Naboo     Droid   <chr> <chr>   
#> 4 Darth Vader     202   136 none       white      yellow          41.9 male  mascu… Tatooine  Human   <chr> <chr>   
   # ℹ 2 more variables: starships <list>, BMI <dbl>

Seules les colonnes sélectionnées par pick sont utilisées dans la mutation, mais toutes les colonnes sont renvoyées.

Les opérations par ligne sont moins courantes pour modifier un seul tibble (la fusion de plusieurs tibbles sera abordée dans un concept ultérieur).

Une exception : arrange() trie les lignes selon les valeurs d'une ou plusieurs colonnes.

tbl
  # A tibble: 4 × 3
#>   languages created has.syllabus
#>   <chr>       <dbl> <lgl>       
#> 1 Fortran      1957 FALSE       
#> 2 R            1993 TRUE        
#> 3 Python       1991 TRUE        
#> 4 Julia        2012 TRUE  

tbl |> arrange(languages)
  # A tibble: 4 × 3
#>   languages created has.syllabus
#>   <chr>       <dbl> <lgl>       
#> 1 Fortran      1957 FALSE       
#> 2 Julia        2012 TRUE        
#> 3 Python       1991 TRUE        
#> 4 R            1993 TRUE     

Résumé

Les dataframes, traditionnels ou tibbles, sont au cœur de l'usage courant du R moderne.

La plupart des fonctions du Tidyverse (pas seulement dplyr) prennent des tibbles en entrée et/ou en produisent en sortie.

Ce concept n'a donné qu'une brève introduction, qui ne fait qu'effleurer ce qui est possible.

D'autres concepts aborderont plusieurs autres aspects des dataframes (dans les limites techniques d'Exercism).

Modifie via GitHub Le lien s'ouvre dans une nouvelle fenêtre ou un nouvel onglet

Apprends Data frames