.. toctree:: :maxdepth: 2 .. include:: weblinks.txt .. _files: Importer des données ==================== Jusqu'à présent, les données utilisées provenaient de datasets inclus dans les packages R et **Tidyverse** ou étaient fournies sous la forme d'objets R. On va explorer l'utilisation de données externes fournies sous la forme de fichiers. La lecture de données est relativement simple en R, à la condition express que les données soient **strictement structurées**. La bonne pratique est d'effectuer le travail de nettoyage et de formattage avec un autre langage, avant d'importer les données dans R. **Python** est un excellent candidat pour ces tâches. Par la suite, les données importées seront structurées pour que l'importation se passe sans difficulté. On va se contenter ici d'importer les données à partir de fichiers texte, mais on trouvera un document exhaustif sur l'importation de données dans le `R Data Import/Export manual `_. Ca couvre également la connexion à des bases de données. La fonction :func:`read.table` ------------------------------ La fonction utilisée pour l'importation de données à partir de fichiers texte est :func:`read.table` dont on trouvera la documentation complète `ici `_. Pour une importation réussie des données dans une ``data.frame``, le fichier texte doit avoir un format bien défini: - la première ligne du fichier doit contenir le nom de chaque variable de la `data.frame`. - chaque ligne supplémentaire doit être composée d'un label et de valeurs pour chacune des variables décrites sur la première ligne. Si cette structuration n'est pas respectée, l'importation peut conduire à des erreurs où à des résultats imprévisibles. A titre d'exemple, un fichier bien formé ressemble à ça:: Price Floor Area Rooms Age Cent.heat 01 52.00 111.0 830 5 6.2 no 02 54.75 128.0 710 5 7.5 no 03 57.50 101.0 1000 5 4.2 no 04 57.50 131.0 690 6 8.8 no 05 59.75 93.0 900 5 1.9 yes ... Le comportement par défaut est que les valeurs numériques sont importées avec le mode ``numeric`` et que les variables non numériques sont importées comme des ``factors`` (``Cent.heat`` dans cet exemple). Ce comportement par défaut peur être modifié en passant des paramètres à la fonction :func:`read.table`. Le fichier utilisé pour cet exemple se trouve :download:`ici `. En utilisant les paramètres par défaut, on construit la ``data.frame`` de la façon suivante:: > HousePrice <- read.table("houses.data") La ``data.frame`` importée:: > HousePrice Price Floor Area Rooms Age Cent.heat 01 52.00 111 830 5 6.2 no 02 54.75 128 710 5 7.5 no 03 57.50 101 1000 5 4.2 no 04 57.50 131 690 6 8.8 no 05 59.75 93 900 5 1.9 yes Il peut arriver que les observations contenues dans le fichier ne disposent pas de labels. Dans ce cas, les labels sont construits à la volée en utilisant les entiers consécutifs. Un fichier sans label pour les observations ressemble à ça:: Price Floor Area Rooms Age Cent.heat 52.00 111.0 830 5 6.2 no 54.75 128.0 710 5 7.5 no 57.50 101.0 1000 5 4.2 no 57.50 131.0 690 6 8.8 no 59.75 93.0 900 5 1.9 yes ... On le lit (presque) de la même façon:: > HousePrice <- read.table("houses.data", header=TRUE) Il faut cependant spécifier le paramètre ``header=TRUE`` pour indiquer que la première ligne contient le nom des variables (c'était implicite précédemment car les lignes d'observations étaient identifiées par un label). La ``data.frame`` importée:: > HousePrice Price Floor Area Rooms Age Cent.heat 1 52.00 111 830 5 6.2 no 2 54.75 128 710 5 7.5 no 3 57.50 101 1000 5 4.2 no 4 57.50 131 690 6 8.8 no 5 59.75 93 900 5 1.9 yes .. admonition:: Exercice Utiliser le fichier :download:`films.csv ` pour construire la ``data.frame`` utilisée dans le chapitre :ref:`factors`. Vérifier attentivement le résultat de la lecture avec la fonction :func:`View`. Utiliser les paramètres de la fonction :func:`read.table` pour corriger d'éventuels dysfonctionnements. Les datasets fournis -------------------- Comme indiqué précedemment, R et **Tidyverse** fournissent un grand nombre de datasets fort utiles pour prendre en main et tester les fonctions de manipulation de données. Pour obtenir une liste de ces datasets:: > data(package = .packages(all.available = TRUE)) Chacun de ces dataset est accessible par son nom (le package doit avoir été importé). On peut à titre d'exemple s'intéresser aux datasets ``Freedman``, ``storms``, ``starwars``, ``gapminder``, ``lakers``, etc... .. code-block:: r > library(lubridate) Attaching package: ‘lubridate’ The following objects are masked from ‘package:base’: date, intersect, setdiff, union > summary(lakers) Application : le Titanic ------------------------ On va s'intéresser aux données relatives au naufrage du `Titanic `_ qui s'est déroulé dans la matinée du 15 avril 1912. On trouvera une source complète d'informations sur l'`Encyclopedia Titanica `__. Créer un nouveau projet dans RStudio. Télécharger :download:`les données ` stockées au format texte (``csv``). Importation ........... Observer la structure du fichier dans un éditeur de texte. Utiliser la fonction :func:`read.table` pour lire les données. .. df <- read.table("titanic.csv", header=TRUE, sep=",") - Quel est la nature de l'objet retourné par cette opération ? - Utiliser la fonction :func:`as_tibble` pour transformer l'objet - Afficher le ``tibble`` ainsi construit - Comment peut on afficher les 20 premières lignes ? - Quelles sont les informations présentes ? - Sont elles toutes renseignées ? - Utiliser les fonctions :func:`summary` et :func:`str` pour avoir un aperçu des données. - Quel est le type de chacune des variables ? - Construire une table de contingence pour chacune des variable. Quelles premières conclusions en tirer ? .. table(t$Age) .. table(t$Class.Dept) .. table(t$Fare.today) .. table(t$Joined) .. table(t$Survived) Nettoyage des données ..................... Il est fréquent qu'un jeu de données contienne des valeurs manquantes. Une opération courante est d'isoler les observations complètes (aucune valeur manquante) ou au contraire les observations incomplètes (au moins une valeur manquante). .. tip:: S'il subsiste des valeurs manquantes dans un vecteur numérique, les fonctions courantes (:func:`mean` par exemple) ont un paramètre ``na.rm`` qui permet d'écarter ces valeurs au moment du traitement. Rechercher les observations incomplètes (avec au moins une valeur manquante) avec la fonction :func:`filter` à laquelle on passera un vecteur logique construit à partir des fonctions :func:`if_any` ou :func:`if_all`, auxquelles on passera deux paramètres: - un opérateur de sélection de variable (la fonction :func:`everything` les prend toutes) - la fonction à appliquer à chacune des variables sélectionnées Le vecteur logique ainsi crée sera réduit par :func:`if_any` ou :func:`if_all` à une seule valeur logique. On consultera: - la documentation de la fonction `filter `_ - la documentation de la fonction `if_any `_ .. Observations avec au moins une valeur manquante : df %>% filter(if_any(everything(), is.na)) # 923 On doit obtenir:: Age Class.Dept Fare.today Joined Survived 1 39 1st Class NA Belfast FALSE 2 NA 1st Class 2390 Southampton FALSE 3 40 1st Class NA Belfast FALSE ... 198 26 Engine NA Belfast FALSE 199 36 Engine NA Southampton FALSE 200 29 Engine NA Southampton FALSE [ reached 'max' / getOption("max.print") -- omitted 723 rows ] Identifier les trois premières observations de ces observations incomplètes. .. df %>% filter(if_any(everything(), is.na)) %>% head(3) On doit obtenir:: Age Class.Dept Fare.today Joined Survived 1 39 1st Class NA Belfast FALSE 2 NA 1st Class 2390 Southampton FALSE 3 40 1st Class NA Belfast FALSE L'index de la ``data.frame`` initiale a été perdu dans l'opération. Pour le conserver, il faut explicitement en faire une variable avant le filtrage avec la fonction :func:`tibble::rownames_to_column`. .. df %>% rownames_to_column('index') %>% filter(if_any(everything(), is.na)) %>% head(3) On doit obtenir:: index Age Class.Dept Fare.today Joined Survived 1 8 39 1st Class NA Belfast FALSE 2 47 NA 1st Class 2390 Southampton FALSE 3 71 40 1st Class NA Belfast FALSE La fonction réciproque :func:`tibble::column_to_rownames` permet de supprimer la variable temporaire et de renommer les observations avec l'index de la ``data.frame`` originale. .. df %>% rownames_to_column('index') %>% filter(if_any(everything(), is.na)) %>% column_to_rownames('index') %>% head(3) On doit obtenir:: Age Class.Dept Fare.today Joined Survived 8 39 1st Class NA Belfast FALSE 47 NA 1st Class 2390 Southampton FALSE 71 40 1st Class NA Belfast FALSE De la même façon, rechercher les observations complètes, sans aucune valeur manquante. Combien y en a t-il ? .. df %>% filter(!if_any(,is.na)) %>% count() # 1285 On doit obtenir:: Age Class.Dept Fare.today Joined Survived 1 29.0 1st Class 16300 Southampton TRUE 2 0.9 1st Class 11700 Southampton TRUE 3 2.0 1st Class 11700 Southampton FALSE ... 206 25.0 1st Class 6350 Cherbourg TRUE 207 65.0 1st Class 2050 Cherbourg FALSE 208 44.0 1st Class 6950 FALSE [ reached 'max' / getOption("max.print") -- omitted 1085 rows ] On peut cibler une (ou plusieurs) variable particulière en la passant en argument de :func:`if_any` ou :func:`if_all`. Rechercher les trois premières observations pour lesquelles l'age est manquant. .. > df %>% rownames_to_column('index') %>% filter(if_any(Age,is.na)) %>% column_to_rownames('index') %>% head(3) On doit obtenir:: Age Class.Dept Fare.today Joined Survived 47 NA 1st Class 2390 Southampton FALSE 180 NA 1st Class 2050 Southampton FALSE 182 NA 1st Class 2700 Southampton FALSE .. tip:: Lorsque peu de variables sont en jeu, on peut construire le vecteur logique plus simplement : .. code-block:: r > df %>% filter(is.na(Age)) La valeur ajoutée de :func:`if_any` ou :func:`if_all` réside dans leur capacité à réduire un grand nombre de valeurs logiques. Rechercher les observations pour lesquelles l'age et le montant du billet sont manquant. .. df %>% rownames_to_column('index') %>% filter(if_all(c(Age,Fare.today),is.na)) %>% column_to_rownames('index') On doit obtenir:: Age Class.Dept Fare.today Joined Survived 1516 NA Engine NA Southampton FALSE 1737 NA Engine NA Southampton FALSE 2153 NA Victualling NA Southampton FALSE Observer les lignes 208, 209 et 210 de la ``data.frame`` originale. Ces lignes sont elles identifiées comme des observations incomplètes ? Pour répondre à cette question: - construire la ``data.frame`` ``inc_obs`` des observations incomplètes. - créer une variable ``index`` à partir du nom des observations avec la fonction :func:`tibble::rownames_to_column`. Quel est le type de la variable ``index`` ? - convertir la variable index en un type propice à la comparaison avec la fonction :func:`mutate` à laquelle on passe la fonction :func:`across` en argument - filtrer selon les observations recherchées Construire l'instruction complète permettant de s'assurer que les lignes 208, 209 et 210 sont, ou ne sont pas dans les observations incomplètes. .. inc_obs %>% rownames_to_column('index') %>% mutate(across(index, as.integer)) %>% filter(index >= 208 & index <= 210) Quelle opération doit on réaliser pour ajouter ce genre d'observation aux observations incomplètes ? Par défaut R affecte la valeur ``NA`` aux variables numériques et logiques pour lesquelles il détecte un champ vide, mais pas aux variables de type ``character``. Utiliser la fonction :func:`mutate` à laquelle on passe la :func:`across` qui prend deux arguments: - les colonnes sélectionnées par position, par nom ou par type avec la fonction :func:`where` - la fonction :func:`na_if` à appliquer, précédée d'un tilde ``~`` pour en faire une formule. ``.`` représente la variable sélectionnée. Pour chacune de ces fonctions, on lira attentivement la documentation et les exemples associés. .. df %>% mutate(across(where(is.character), ~na_if(.,""))) .. df %>% mutate(across(Joined, ~na_if(.,""))) .. df %>% mutate(across(4, ~na_if(.,""))) .. tip:: Il est souvent plus efficace de convertir en NA les valeurs concernées lors du processus de lecture. N'y a t'il pas un paramètre de la fonction :func:`read.table` qui permet de considérer les chaines de caractères vides comme des valeurs ``NA`` ? .. df <- read.table("titanic.csv", header=TRUE, sep=",", na.strings="") Construire la ``data.frame`` ``com_obs`` des observations complètes. .. com_obs <- df %>% rownames_to_column('index') %>% filter(!if_any(everything(), is.na)) %>% column_to_rownames('index') Observer le résultat de la fonction :func:`summary` appliquée à la ``data.frame`` des observations complètes. On doit obtenir:: Age Class.Dept Fare.today Joined Min. : 0.00 1st Class:310 Min. : 245 Belfast : 1 1st Qu.:21.00 2nd Class:271 1st Qu.: 612 Cherbourg :271 Median :27.00 3rd Class:701 Median : 1120 Queenstown :120 Mean :29.32 Mean : 2600 Southampton:890 3rd Qu.:37.00 3rd Qu.: 2420 Max. :74.00 Max. :39600 Survived Mode :logical FALSE:786 TRUE :496 Est ce le cas ? Si non, quelles variables ne produisent pas les résultats escomptés. Comment peut on les modifier avec la fonction :func:`mutate` ? .. com_obs %>% mutate(across(where(is.character), as.factor)) %>% summary() .. tip:: Il existe un paramètre de la fonction :func:`read.table` permettant de convertir les chaines de caractères en facteurs à la lecture des données. .. df <- read.table("titanic.csv", header=TRUE, sep=",", na.strings="", stringsAsFactors = TRUE) Transformation .............. L'`Encyclopedia Titanica `__ fournit un certain nombre de statistiques. Nous allons en reconstruire certaines en utilisant R et **Tidyverse**. Il peut y avoir des écarts mineurs dans les résultats obtenus, dus à des écarts entre les jeux de données utilisés. On va utiliser les ``data.frame`` précédentes pour tirer quelques conclusions et tracer quelques graphiques. Combien de passagers ont embarqué à Belfast, Southampton ? Cherbourg ? Queenstown ? .. table(titanic$Joined) .. p <- ggplot(remove_missing(titanic, vars="Joined"), aes(x=Joined, fill=Joined)) .. p <- p + geom_histogram(stat="count") .. p Construire la figure ci dessous. .. image:: images/fr-13-joined.png :scale: 80 % :align: center .. tip:: La variable ``Joined`` contient des ``NA``. On peut les éliminer en amont en construisant une ``data.frame`` spécifique. On peut également les écarter au moment de la construction de l'objet ``ggplot`` avec la fonction :func:`remove_missing` : `la documentation `__. Tracer l'histogramme de l'age des passagers pour chaque classe. .. > p <- ggplot(data= titanic %>% filter(Class.Dept=="1st Class" | Class.Dept=="2nd Class" | Class.Dept=="3rd Class"), aes(x=Age, fill=Class.Dept)) > p <- p + geom_histogram(breaks=seq(0, 70, by = 10)) > p <- p + facet_wrap(~Class.Dept, ncol=1) > p .. image:: images/11-files-fig01.png :scale: 80 % :align: center .. tip:: On peut utiliser la fonction :func:`stringr::str_detect` pour sélectionner les observations de la variable ``Class.Dept`` Quels sont la moyenne, la médiane, et l'écart type le la variable ``Age`` pour chacune des valeurs de la variable ``Class.Dept`` ? On utilisera la fonction :func:`group_by` pour le groupement de données et la fonction :func:`summarize` pour la réduction. Construire le flux de traitement conduisant à :: # A tibble: 7 x 2 Class.Dept mean 1 1st Class 39.1 2 2nd Class 29.6 3 3rd Class 24.9 4 A la Carte 25.2 5 Deck 33.8 6 Engine 30.4 7 Victualling 31.3 .. > titanic %>% group_by(Class.Dept) %>% summarize(mean=mean(Age, na.rm=TRUE)) .. tip:: Les fonctions statistiques comme :func:`mean` acceptent le paramètre ``na.rm=TRUE`` pour écarter les ``NA`` avant traitement. Modifier le groupement pour obtenir l'age moyen des survivants et des morts pour chacune des classes. .. > titanic %>% group_by(Class.Dept, Survived) %>% summarize(mean=mean(Age, na.rm=TRUE)) Compter le nombre de morts et de survivants pour chacune des classes de passagers et comparer les pourcentages. .. code-block:: r # A tibble: 14 x 4 # Groups: Class.Dept [7] Class.Dept Survived n percent 1 1st Class FALSE 123 0.380 2 1st Class TRUE 201 0.620 3 2nd Class FALSE 166 0.582 4 2nd Class TRUE 119 0.418 5 3rd Class FALSE 528 0.746 6 3rd Class TRUE 180 0.254 7 A la Carte FALSE 66 0.957 8 A la Carte TRUE 3 0.0435 9 Deck FALSE 23 0.348 10 Deck TRUE 43 0.652 11 Engine FALSE 253 0.778 12 Engine TRUE 72 0.222 13 Victualling FALSE 337 0.782 14 Victualling TRUE 94 0.218 Source: local data frame [14 x 4] Groups: Class.Dept [7] .. > titanic %>% group_by(Class.Dept, Survived) %>% summarize(n=n()) %>% mutate(percent=n/sum(n)) A partir du ``tibble`` précédent, tracer un barplot pour mettre en évidence le pourcentage de survivant pour chacune des catégories de passagers. .. > p <- ggplot(data=c, aes(x=Class.Dept, y=percent, fill=Survived)) > p <- p + geom_bar(stat="identity") > p .. image:: images/11-files-fig02.png :scale: 80 % :align: center