.. toctree:: :maxdepth: 2 .. include:: weblinks.txt .. _transforming: Transformation de données ========================= Les jeux de données disponibles ne sont pas tous bien formés pour l'analyse. On va s'intéresser dans ce chapitre à la constitution de data frames optimisées pour l'analyse de données et la construction de graphiques avec ``ggplot2``. Un dataset mal formé -------------------- Comme on l'a vu à la fin du chapitre :ref:`dataframes`, les données de pollution mises à disposition par `AirParif `_ ne sont pas structurées de façon optimale et la transformation est un enjeu majeur, sans quoi la présentation et l'analyse en seront au mieux difficiles, au pire impossibles. Le problème dans ce dataset provient du fait que la nature du polluant dont on mesure la concentration n'est pas accessible comme une observation et ne peut pas être traité automatiquement par les packages de R. .. code-block:: r > head(air) date heure PM10.microg.m3. NO2.microg.m3. O3.microg.m3. 1 2015-04-01 1 21 8 79 2 2015-04-01 2 18 9 78 3 2015-04-01 3 16 10 75 4 2015-04-01 4 14 12 69 5 2015-04-01 5 8 21 60 6 2015-04-01 6 10 33 49 Un dataset bien formé mais pas optimal -------------------------------------- Le dataset ``iris`` ne souffre pas du même défaut, la variable catégorielle étant ici une variable à part entière:: > head(iris) Sepal.Length Sepal.Width Petal.Length Petal.Width Species 1 5.1 3.5 1.4 0.2 setosa 2 4.9 3.0 1.4 0.2 setosa 3 4.7 3.2 1.3 0.2 setosa 4 4.6 3.1 1.5 0.2 setosa 5 5.0 3.6 1.4 0.2 setosa 6 5.4 3.9 1.7 0.4 setosa On parlera ici de dataset ``tidy`` puisque toutes les informations sont accessibles à travers des variables. Les règles d'un dataset ``tidy`` sont schématisées sur la figure suivante(source: `R for Data Science `_). .. image:: images/09-dataframes-fig12.png :scale: 80 % :align: center Lire attentivement `l'article de Hadley Wickham `_ pour une explication détaillée. Comprendre l'organisation des données est une étape fondamentale pour un traitement performant. Un dataset optimal ------------------ Les données du dataset ``iris`` sont pleinement fonctionnelles (au sens qu'elles respectent l'organisation ``tidy``) parce que l'ensemble des informations sont stockées dans des variables. Ces données sont organisées sous la forme ``wide``, ce qui pourrait s'imager en disant qu'elles sont présentées en mode paysage. L'autre format possible est le format ``long`` (par opposition à ``wide``), et pour poursuivre l'analogie précédente, on pourrait dire que les données sont alors présentées en mode portrait. Alors que le format ``wide`` est intéressant pour l'observation des données par un oeil humain, le format ``long`` sera plus adapté à un traitement par la machine et sera privilégié pour plusieurs raisons: - le format ``long`` s'affiche plus facilement dans une fenêtre de taille réduite et évite le scrolling horizontal - les données sont structurées en paires clé-valeur, ce qui les rend plus facilement échangeables avec d'autres langages de programmation ou d'autres formats. Pour Python, la structuration clé-valeur est celle des dictionnaires. Pour le web, cette structuration facilite le portage au format JSON, etc... - ``ggplot2`` et les packages de **Tidyverse** sont conçus pour fonctionner avec des data frames utilisant le format ``long``. La nécessité de modifier le format des données ---------------------------------------------- Il est extrémement rare, voire illusoire, de trouver des données dans le format optimal pour leur traitement. Ca tient à la grande diversité des producteurs de données, ainsi qu'aux différents métiers impliqués. Les données `AirParif `_ utilisées dans le chapitre :ref:`dataframes` en sont une illustration. Transformer le format des données pourra impliquer une ou plusieurs des actions suivantes: - transformation du format ``wide`` au format ``long`` (ou réciproquement le cas échéant) - sélectionner les observations selon les valeurs d'une ou plusieurs variables - filtrer les variables à retenir pour l'analyse - trier les observations suivant un ou plusieurs critères - créer de nouvelles variables - modifier certaines variables - effectuer une réduction statistique des données - combiner deux ou plusieurs jeux de données - etc... Certaines de ces opérations ont été effectuées dans les chapitres précédents, souvent avec des opérations à bas niveau ou manipulant les fonctions R core. Nous allons aborder les transformations à plus haut niveau en utilisant les packages **Tidyverse**. Du format ``wide`` au format ``long`` ------------------------------------- Pour le moment la ``data.frame`` ``air`` est au format ``wide``:: > head(air) date heure PM10.microg.m3. NO2.microg.m3. O3.microg.m3. 1 2015-04-01 1 21 8 79 2 2015-04-01 2 18 9 78 3 2015-04-01 3 16 10 75 4 2015-04-01 4 14 12 69 5 2015-04-01 5 8 21 60 6 2015-04-01 6 10 33 49 L'objectif est de transformer cette ``data.frame`` au format ``long`` pour profiter pleinement des possibilités du package ``ggplot2``. La fonction :func:`tidyr::pivot_longer` est la fonction appropriée (voir `la documentation `__). Les principaux paramètres de la fonction :func:`tidyr::pivot_longer` sont les suivants: - la ``data.frame`` originale au format ``wide`` - les variables sélectionnées pour la transformation - ``names_to`` : le nom de la nouvelle variable qui remplace les variables sélectionnées pour la transformation. Ces dernières deviennent les valeurs de cette nouvelle variable. - ``values_to`` : le nom de la nouvelle variable qui va héberger les valeurs des variables transformées Un exemple pour illustrer ça: .. code-block:: r > air_long <- pivot_longer(air, !c("date", "heure"), names_to = "pollutant", values_to = "value") - la ``data.frame`` est ``air`` - on transforme toutes les variables à l'exception des variables ``date`` et ``heure`` - la nouvelle variable (dont les valeurs seront le nom des variables transformées ``PM10.microg.m3.``, ``NO2.microg.m3.``, ``O3.microg.m3.``) sera nommée ``pollutant`` - la nouvelle variable qui contiendra les valeurs des variables transformées sera nommée ``value`` le résultat de la transformation:: > head(air_long) # A tibble: 6 x 4 date heure pollutant value 1 2015-04-01 1 PM10.microg.m3. 21 2 2015-04-01 1 NO2.microg.m3. 8 3 2015-04-01 1 O3.microg.m3. 79 4 2015-04-01 2 PM10.microg.m3. 18 5 2015-04-01 2 NO2.microg.m3. 9 6 2015-04-01 2 O3.microg.m3. 78 > tail(air_long) # A tibble: 6 x 4 date heure pollutant value 1 2015-04-30 23 PM10.microg.m3. 3 2 2015-04-30 23 NO2.microg.m3. 11 3 2015-04-30 23 O3.microg.m3. 61 4 2015-04-30 24 PM10.microg.m3. 2 5 2015-04-30 24 NO2.microg.m3. 8 6 2015-04-30 24 O3.microg.m3. 66 Un aperçu de la ``data.frame`` transformée pour visualiser l'ordonnancement de la transformation. Chaque observation de la ``data.frame`` initiale est traitée séquentiellement. :: > air_long[715:723,] # A tibble: 9 x 4 date heure pollutant value 1 2015-04-10 23 PM10.microg.m3. 21 2 2015-04-10 23 NO2.microg.m3. 12 3 2015-04-10 23 O3.microg.m3. 87 4 2015-04-10 24 PM10.microg.m3. 20 5 2015-04-10 24 NO2.microg.m3. 8 6 2015-04-10 24 O3.microg.m3. 88 7 2015-04-11 1 PM10.microg.m3. 14 8 2015-04-11 1 NO2.microg.m3. 7 9 2015-04-11 1 O3.microg.m3. 85 Cette opération de transformation génère un ``tibble``, une version étendue de la ``data.frame``. Par rapport à la ``data.frame`` originale, un ``tibble`` apporte des informations supplémentaires, notamment sur le type des variables. On voit ici que le polluant est une simple chaine de caractère alors qu'un facteur serait plus approprié pour effectuer des groupements. Transformons donc la variable ``pollutant`` en facteur: .. code-block:: r > air_long$pollutant <- as.factor(air_long$pollutant) et vérifions la validité de la transformation:: > head(air_long) # A tibble: 6 x 4 date heure pollutant value 1 2015-04-01 1 PM10.microg.m3. 21 2 2015-04-01 1 NO2.microg.m3. 8 3 2015-04-01 1 O3.microg.m3. 79 4 2015-04-01 2 PM10.microg.m3. 18 5 2015-04-01 2 NO2.microg.m3. 9 6 2015-04-01 2 O3.microg.m3. 78 Les valeurs de la variable ``pollutant`` ne sont pas très explicites et incluent l'unité de mesure. Remettons un peu d'ordre dans tout ça. En modifiant l'intitulé des ``levels`` du facteur ``pollutant``: .. code-block:: r > levels(air_long$pollutant) <- c("NO2","O3", "PM10") et en transférant l'unité de mesure dans la variable ``value``:: > colnames(air_long)[4] <- "µg.per.m3" Au final, la ``data.frame`` ressemble à:: > head(air_long) # A tibble: 6 x 4 date heure pollutant µg.per.m3 1 2015-04-01 1 PM10 21 2 2015-04-01 1 NO2 8 3 2015-04-01 1 O3 79 4 2015-04-01 2 PM10 18 5 2015-04-01 2 NO2 9 6 2015-04-01 2 O3 78 Elle est maintenant prête pour la construction de graphiques: .. code-block:: r > p <- ggplot(air_long, aes(x=pollutant, y=µg.per.m3, fill=pollutant)) > p <- p + geom_boxplot() > p .. image:: images/10-transforming-fig01.png :scale: 80 % :align: center Filtrer la ``data.frame`` ------------------------- On peut sélectionner une partie de la ``data.frame`` en effectuant une opération de filtrage sur les observations. Les observations sont filtrées en utilisant un ou plusieurs prédicats impliquant une ou plusieurs variables. Les observations retenues sont celle pour lesquelles l'ensemble des prédicats est évalué à ``TRUE``. Cette opération est effectuée avec la fonction :func:`dplyr::filter`. Voir `la documentation du package dplyr `_. On peut extraire toutes les mesures d'ozone effectuées à 12:00 pour le mois d'avril 2015:: > filter(air_long, heure==12, pollutant=="O3") date heure pollutant µg.per.m3 1 2015-04-01 12 O3 77 2 2015-04-02 12 O3 51 3 2015-04-03 12 O3 27 ... 28 2015-04-28 12 O3 62 29 2015-04-29 12 O3 76 30 2015-04-30 12 O3 44 On peut construire des prédicats complexes en utilisant les opérateurs logiques:: > filter(air_long, heure==12, pollutant=="O3" | pollutant=="PM10") date heure pollutant µg.per.m3 1 2015-04-01 12 PM10 20 2 2015-04-02 12 PM10 2 3 2015-04-03 12 PM10 15 ... 58 2015-04-28 12 O3 62 59 2015-04-29 12 O3 76 60 2015-04-30 12 O3 44 Sélection de variables ---------------------- Le filtrage vu précédemment est l'opération de sélection d'observations (lignes). Les variables (colonnes) sont sélectionnées avec la fonction :func:`select`:: > select(air_long, -date) heure pollutant µg.per.m3 1 1 PM10 21 2 2 PM10 18 3 3 PM10 16 4 4 PM10 14 5 5 PM10 8 ... 329 17 PM10 9 330 18 PM10 10 331 19 PM10 12 332 20 PM10 17 333 21 PM10 16 [ reached getOption("max.print") -- omitted 1827 rows ] L'utilisation de l'opérateur de négation vu dans le chapitre :ref:`vectors` est permis. Tri des observations -------------------- La plupart du temps, les données stockées dans une ``data.frame`` sont destinées à être traitées programmatiquement et l'ordonnancement des données n'est pas primordial. Mais l'examen des données dans la console (avec les fonctions :func:`head`, :func:`tail`, etc...) peut se trouver facilité par une organisation différente de l'organisation brute du jeu de données. La fonction :func:`arrange` permet ça. Pour le moment, la ``data.frame`` ``air_long`` est ordonnée par la variable ``pollutant``. S'il est plus judicieux de présenter les données de façon temporelle, on passe les paramètres ``date`` et ``heure`` (utilisés dans cet ordre) à la fonction :func:`arrange`:: > arrange(air_long, date, heure) date heure pollutant µg.per.m3 1 2015-04-01 1 PM10 21 2 2015-04-01 1 NO2 8 3 2015-04-01 1 O3 79 4 2015-04-01 2 PM10 18 5 2015-04-01 2 NO2 9 6 2015-04-01 2 O3 78 ... 247 2015-04-04 11 PM10 10 248 2015-04-04 11 NO2 20 249 2015-04-04 11 O3 19 250 2015-04-04 12 PM10 17 [ reached getOption("max.print") -- omitted 1910 rows ] Créer ou modifier une variable ------------------------------ La ``data.frame`` doit parfois être modifiée pour permettre (ou faciliter) le traitement des données qu'elle contient. A titre d'exemple la ``data.frame`` ``air_long`` structure les informations temporelles en deux variables distinctes ``date`` et ``heure``. Cette séparation du timestamp en deux variables rend complexe les opérations de calcul temporel. Difficile par exemple de calculer le temps écoulé entre deux observations. R possède la classe :class:`Posixct` spécialement conçue pour répondre à ce genre de problème (voir `la documentation `__.). L'objectif est donc de créer un objet de type :class:`Posixct` à partir des variables ``date`` et ``heure``. La fonction :func:`dplyr::mutate` est utilisée pour créer de nouvelles variables ou modifier des variables existantes (voir `la documentation `__). On modifie la classe de la variable ``date`` avec la fonction de coercition :func:`as.POSIXct`. Le paramètre ``format`` utilise des variables réservées pour définir le formattage à partir de l'objet ``date``. ``%Y`` fait référence à l'année au format long, ``%m`` fait référence au mois présenté avec un format réduit, etc...:: > air_final <- mutate(air_long, date=as.POSIXct(date, format="%Y%m%d %H%M%S")) > head(air_final) # A tibble: 6 x 4 date heure pollutant µg.per.m3 1 2015-04-01 02:00:00 1 PM10 21 2 2015-04-01 02:00:00 1 NO2 8 3 2015-04-01 02:00:00 1 O3 79 4 2015-04-01 02:00:00 2 PM10 18 5 2015-04-01 02:00:00 2 NO2 9 6 2015-04-01 02:00:00 2 O3 78 On note l'apparition d'un type ``dttm`` dans l'affichage du ``tibble``. Ce type est créé par la fonction de coercition :func:`as.POSIXct`. La variable ``date`` intégre maintenant une information de temps (heure, minute, seconde) mais comme elle n'est pas disponible dans la valeur originale, une valeur par défaut est utilisée: .. code-block:: r > sapply(air_final, class) $date [1] "POSIXct" "POSIXt" $heure [1] "factor" $pollutant [1] "factor" $µg.per.m3 [1] "numeric" La fonction :func:`lubridate::hour` est utilisé pour accéder (en lecture) à l'attribut :attr:`hour` de la variable ``date``.Pour le moment, on ne récupère que les valeurs par défaut mais on a identifié une façon d'accéder à cette information: .. code-block:: r > hour(air_final$date) [1] 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 ... [ reached getOption("max.print") -- omitted 1160 entries ] On va utiliser la variable ``heure`` à droite de l'opérateur d'affectation pour écrire dans l'attribut :attr:`hour` de la variable ``date``: .. code-block:: r > hour(air_final$date) <- as.numeric(air_final$heure) > head(air_final) # A tibble: 6 x 4 date heure pollutant µg.per.m3 1 2015-04-01 01:00:00 1 PM10 21 2 2015-04-01 01:00:00 1 NO2 8 3 2015-04-01 01:00:00 1 O3 79 4 2015-04-01 02:00:00 2 PM10 18 5 2015-04-01 02:00:00 2 NO2 9 6 2015-04-01 02:00:00 2 O3 78 Il reste deux étapes pour obtenir une ``data.frame`` parfaitement *tidy*: - la variable ``heure`` est redondante et doit être supprimée - le nom de la variable ``date`` ne reflète plus l'information qu'il contient et doit être modifié. On supprime une variable en lui affectant ``NULL``: .. code-block:: r > air_final$heure <- NULL Les noms de variables sont accessibles avec la fonction :func:`colnames`:: > colnames(air_final)[1] <- "date_time" Au final, après ces deux dernières transformations, la ``data.frame`` est correctement formée et prête à l'analyse:: > head(air_final) # A tibble: 6 x 3 date_time pollutant µg.per.m3 1 2015-04-01 01:00:00 PM10 21 2 2015-04-01 01:00:00 NO2 8 3 2015-04-01 01:00:00 O3 79 4 2015-04-01 02:00:00 PM10 18 5 2015-04-01 02:00:00 NO2 9 6 2015-04-01 02:00:00 O3 78 ``data.frame`` ou ``tibble`` ---------------------------- On a déjà rencontré l'objet ``tibble`` dans le chapitre :ref:`overview`. En fait les deux structures de données sont très proches l'une de l'autre, et un ``tibble`` peut être vu comme une version améliorée de la ``data.frame``, avec essentiellement deux apports/modifications: - l'affichage dans la console est plus efficace et le type des variables est indiqué - la syntaxe de sélection d'une sous partie de la structure est plus stricte. En particulier l'abbréviation du nom des variables ne fonctionne pas. Passer de l'un à l'autre est direct avec les fonctions de coercition:: > df <- as.data.frame(air_final) > head(df) date_time pollutant µg.per.m3 1 2015-04-01 01:00:00 PM10 21 2 2015-04-01 01:00:00 NO2 8 3 2015-04-01 01:00:00 O3 79 4 2015-04-01 02:00:00 PM10 18 5 2015-04-01 02:00:00 NO2 9 6 2015-04-01 02:00:00 O3 78 > class(df) [1] "data.frame" La fonction :func:`tibble::as_tibble` permet la conversion réciproque:: > library(tibble) > tb <- as_tibble(df) > head(tb) # A tibble: 6 x 3 date_time pollutant µg.per.m3 1 2015-04-01 01:00:00 PM10 21 2 2015-04-01 01:00:00 NO2 8 3 2015-04-01 01:00:00 O3 79 4 2015-04-01 02:00:00 PM10 18 5 2015-04-01 02:00:00 NO2 9 6 2015-04-01 02:00:00 O3 78 La fonction :class:`class` appliquée à un objet de type ``tibble`` montre que c'est une ``data.frame`` "augmentée". .. code-block:: r > class(tb) [1] "tbl_df" "tbl" "data.frame" En conclusion, travailler avec l'une ou l'autre de ces structures importe peu, mais le ``tibble`` est une version plus moderne de la ``data.frame`` et ce sera une bonne pratique de l'utiliser. Réduction de données -------------------- La réduction de données est l'opération qui consiste à représenter un ensemble de valeurs par une seule (moyenne, médiane, écart type, etc...). Les données, structurées dans un objet ``tibble``, sont maintenant prêtes pour la création de graphique: .. code-block:: r > p <- ggplot(air_final, aes(x=date_time, y=µg.per.m3, group=pollutant, color=pollutant)) > p <- p + geom_line() > p Ce premier graphique n'est pas très explicite, à cause de la forte densité d'informations et à la périodicité temporelle. .. image:: images/10-transforming-fig02.png :scale: 80 % :align: center Il nous faut construire des visualisations plus explicites des données de pollution, permettant de tirer des conclusions sans ambigüité. Une première idée est de séparer les jours ouvrés (lundi à vendredi) de ceux du week end. On peut attendre qu'il y ait moins de traffic le week end que la semaine, bien que le samedi puisse être utilisé pour des activités commerciales. On peut utiliser la fonction :func:`lubridate::wday` qui retourne un entier entre ``1`` et ``7`` associé au jour de la semaine (dimanche = 1): .. code-block:: r > air_WorkingDays <- filter(air_final, wday(date_time)!= 1 & wday(date_time)!= 7) > air_WeekEnd <- filter(air_final, wday(date_time)== 1 | wday(date_time)== 7) On peut vérifier la validité de l'opération en récupérant les valeurs uniques des dates référencées dans chacun des sous ``tibble``. Pour celui qui concerne les jours ouvrés, les 4 et 5 avril 2015 sont manquants: .. code-block:: r > unique(date(air_WorkingDays$date_time)) [1] "2015-04-01" "2015-04-02" "2015-04-03" "2015-04-06" "2015-04-07" [6] "2015-04-08" "2015-04-09" "2015-04-10" "2015-04-13" "2015-04-14" [11] "2015-04-15" "2015-04-16" "2015-04-17" "2015-04-20" "2015-04-21" [16] "2015-04-22" "2015-04-23" "2015-04-24" "2015-04-27" "2015-04-28" [21] "2015-04-29" "2015-04-30" "2015-05-01" Pour celui qui concerne les week ends: .. code-block:: r > unique(date(air_WeekEnd$date_time)) [1] "2015-04-04" "2015-04-05" "2015-04-11" "2015-04-12" "2015-04-18" [6] "2015-04-19" "2015-04-25" "2015-04-26" Pour calculer la valeur moyenne pour chacun des polluants, on a besoin de deux nouvelles fonctions: - la fonction :func:`dplyr::group_by` utilisé pour effectuer des groupements de données - la fonction :func:`dplyr::summarize`, appliquée aux données groupées, pour effectuer la réduction de données proprement dite La première étape concerne le groupement: .. code-block:: r > g <- group_by(air_WorkingDays, pollutant, hour(date_time)) > g # A tibble: 1,584 x 4 # Groups: pollutant, hour(date_time) [72] date_time pollutant µg.per.m3 `hour(date_time)` 1 2015-04-01 01:00:00 PM10 21 1 2 2015-04-01 01:00:00 NO2 8 1 3 2015-04-01 01:00:00 O3 79 1 4 2015-04-01 02:00:00 PM10 18 2 5 2015-04-01 02:00:00 NO2 9 2 6 2015-04-01 02:00:00 O3 78 2 7 2015-04-01 03:00:00 PM10 16 3 8 2015-04-01 03:00:00 NO2 10 3 9 2015-04-01 03:00:00 O3 75 3 10 2015-04-01 04:00:00 PM10 14 4 # ... with 1,574 more rows On note l'information sur le groupement affiché dans l'en tête du ``tibble``. La seconde étape consiste à effectuer la réduction de données: .. code-block:: r > s <- summarize(g, mean=mean(µg.per.m3, na.rm=TRUE)) `summarise()` regrouping output by 'pollutant' (override with `.groups` argument) > s # A tibble: 72 x 3 # Groups: pollutant [3] pollutant `hour(date_time)` mean 1 NO2 0 45.8 2 NO2 1 37.7 3 NO2 2 33.0 4 NO2 3 32.2 5 NO2 4 36.5 6 NO2 5 49.1 7 NO2 6 54.7 8 NO2 7 52.4 9 NO2 8 39.1 10 NO2 9 33.0 # ... with 62 more rows Puisque chaque fonction du package ``dplyr`` prend une ``data.frame`` en entrée et retourne également une ``data.frame``, on peut utiliser l'opérateur de pipe ``%>%`` pour écrire de façon compacte et élégante le processus d'analyse de données: .. code-block:: r > swd <- air_WorkingDays %>% + group_by(pollutant, hour(date_time)) %>% + summarize(mean=mean(µg.per.m3, na.rm=TRUE)) `summarise()` regrouping output by 'pollutant' (override with `.groups` argument) > swd # A tibble: 72 x 3 # Groups: pollutant [3] pollutant `hour(date_time)` mean 1 NO2 0 45.8 2 NO2 1 37.7 3 NO2 2 33.0 4 NO2 3 32.2 5 NO2 4 36.5 6 NO2 5 49.1 7 NO2 6 54.7 8 NO2 7 52.4 9 NO2 8 39.1 10 NO2 9 33.0 # ... with 62 more rows .. important:: Lorsqu'on évoque les structures de données produites par les fonctions des packages **Tidyverse**, on parle de ``data.frame`` au sens large. Cette dénomination inclut aussi les ``tibble``. Cette écriture est particulièrement lisible, car les données produites à gauche du pipe sont utilisées comme entrée pour la fonction située à droite. Le graphique est maintenant simple à produire: .. code-block:: r > p <- ggplot(swd, aes(x=`hour(date_time)`, y=mean, group=pollutant, color=pollutant)) > p <- p + geom_line() > p <- p + ggtitle("Working days") > p .. image:: images/10-transforming-fig03.png :scale: 80 % :align: center On peut effectuer le même traitement pour les observations effectuées le week end. Il faut d'abord construire la ``data.frame``: .. code-block:: r > swe <- air_WeekEnd %>% + group_by(pollutant, hour(date_time)) %>% + summarize(mean=mean(µg.per.m3, na.rm=TRUE)) `summarise()` regrouping output by 'pollutant' (override with `.groups` argument) pour afficher les données réduites: .. code-block:: r > p <- ggplot(swe, aes(x=`hour(date_time)`, y=mean, group=pollutant, color=pollutant)) > p <- p + geom_line() > p <- p + ggtitle("Week ends") > p .. image:: images/10-transforming-fig04.png :scale: 80 % :align: center Il est intéressant de superposer les deux graphes. Pour travailler de façon optimale avec ``ggplot2``, la bonne pratique est de construire un seul dataset intégrant une variable logique ``weekend`` qui pourrait servir à grouper les données. Combiner les data frames ------------------------ On dispose pour le moment de deux ``data.frames`` distinctes contenant pour l'une les données concernant les jours ouvrés, et pour l'autre les données concernant les week end. Cependant l'opération de froupement effectuée par la fonction :func:`group_by` a produit une variable dont le nom ``hour(date_time)`` n'est pas optimal. Modifions le avant de combiner les ``data.frame``:: > colnames(swe)[2] <- colnames(swd)[2] <- "hour" > swe # A tibble: 72 x 3 # Groups: pollutant [3] pollutant hour mean 1 NO2 0 29.9 2 NO2 1 28.4 3 NO2 2 25.6 4 NO2 3 23.5 5 NO2 4 21.6 6 NO2 5 21.1 7 NO2 6 21.1 8 NO2 7 22.1 9 NO2 8 18.2 10 NO2 9 14.2 # ... with 62 more rows .. important:: Remarquer ici la double affectation. On a vu que la fonction :func:`dplyr::mutate` pouvait être utilisée pour modifier une variable ou en créer une. On utilise ici cette deuxième possibilité pour ajouter une variable ``when`` qui contiendra l'information de la nature du jour de l'observation:: > swd <- mutate(swd, when="WorkingDays") > swe <- mutate(swe, when="WeekEnd") Maintenant les ``data.frame`` ``swd`` et ``swe`` sont prêtes à être combinées:: > swe # A tibble: 72 x 4 # Groups: pollutant [3] pollutant hour mean when 1 NO2 0 29.9 WeekEnd 2 NO2 1 28.4 WeekEnd 3 NO2 2 25.6 WeekEnd 4 NO2 3 23.5 WeekEnd 5 NO2 4 21.6 WeekEnd 6 NO2 5 21.1 WeekEnd 7 NO2 6 21.1 WeekEnd 8 NO2 7 22.1 WeekEnd 9 NO2 8 18.2 WeekEnd 10 NO2 9 14.2 WeekEnd # ... with 62 more rows > swd # A tibble: 72 x 4 # Groups: pollutant [3] pollutant hour mean when 1 NO2 0 45.8 WorkingDays 2 NO2 1 37.7 WorkingDays 3 NO2 2 33.0 WorkingDays 4 NO2 3 32.2 WorkingDays 5 NO2 4 36.5 WorkingDays 6 NO2 5 49.1 WorkingDays 7 NO2 6 54.7 WorkingDays 8 NO2 7 52.4 WorkingDays 9 NO2 8 39.1 WorkingDays 10 NO2 9 33.0 WorkingDays # ... with 62 more rows .. tip:: Le package ``dplyr`` contient beaucoup de fonctions permettant de combiner les ``data.frame``. Pour une vue complète de la combinaison de ``data.frame``, on s'intéressera aux fonctions de la famille :func:`xxx_join` décrites dans `la documentation de dplyr `_. Ces fonctions reprennent le vocabulaire des jointures de tables SQL. Une explication assez complète dans l'article `Joining Data in R with dplyr `_. La fonction :func:`dplyr::bind_rows` est ici bien adaptée à notre cas puisque les ``data.frame`` sont de structure identique:: > m <- bind_rows(swd, swe) > m # A tibble: 144 x 4 # Groups: pollutant [3] pollutant hour mean when 1 NO2 0 45.8 WorkingDays 2 NO2 1 37.7 WorkingDays 3 NO2 2 33.0 WorkingDays 4 NO2 3 32.2 WorkingDays 5 NO2 4 36.5 WorkingDays 6 NO2 5 49.1 WorkingDays 7 NO2 6 54.7 WorkingDays 8 NO2 7 52.4 WorkingDays 9 NO2 8 39.1 WorkingDays 10 NO2 9 33.0 WorkingDays # ... with 134 more rows Comme le dit le proverbe, ``data.frame`` bien préparée, production de graphiques facilitée ;-) :: > p <- ggplot(data=m, aes(x=hour, y=mean, color=pollutant, linetype=when)) > p <- p + geom_line(size=1) > p <- p + ggtitle("Air pollution in Lognes (A4)") > p Le graphique montre très clairement l'évolution de la moyenne de chacun des polluants. Pour étendre l'étude, il serait intéressant d'utiliser les données météorologiques correspondantes, dont on sait qu'elles ont un impact fort. .. image:: images/10-transforming-fig05.png :scale: 80 % :align: center Application : population urbaine -------------------------------- Reprenons les données des 20 plus grandes villes françaises utilisées dans les chapitres précédents. Ces données ont été structurées dans une ``matrix`` dont on a une vue partielle ci dessous:: Angers Bordeaux Brest Dijon Grenoble Le.Havre Le.Mans ... 1962 115273 278403 136104 135694 156707 187845 132181 ... 1968 128557 266662 154023 145357 161616 207150 143246 ... 1975 137591 223131 166826 151705 166037 217882 152285 ... 1982 136038 208159 156060 140942 156637 199388 147697 ... 1990 141404 210336 147956 146703 150758 195854 145502 ... 1999 151279 215363 149634 149867 153317 190905 146105 ... 2007 151108 235178 142722 151543 156793 179751 144164 ... 2012 149017 241287 139676 152071 158346 173142 143599 ... Pour tracer quelques graphiques, ``ggplot2`` nécessite des objets de type ``data.frame``. Utiliser la fonction :func:`as.data.frame` pour transformer cette matrice (construite dans le chapitre :ref:`arrays`) en une ``data.frame``. .. df = as.data.frame(mt) Cette ``data.frame`` est fonctionnelle au sens où toutes les données sont accessibles mais elle présente plusieurs défauts: - elle est au format ``wide`` qui n'est pas optimal pour la construction de graphiques - une donnée fondamentale (l'année) n'est pas une variable mais le nom de l'observation. Elle n'est pour le moment pas utilisable pour effectuer des groupements de données. Appliquer les transformations nécessaires pour supprimer ces deux inconvénients. On nommera ``dfl`` la ``data.frame`` transformée. .. df <- tibble::rownames_to_column(df, "year") .. dfl <- tidyr::pivot_longer(df, -year, names_to="city", values_to="pop") .. dfl <- dfl <- dplyr::mutate(dfl, across(c(year,city), factor)) Après transformation, la ``data.frame`` ``dfl`` se présente maintenant de la façon suivante:: > head(dfl, 10) # A tibble: 10 x 3 year city pop 1 1962 Angers 115273 2 1962 Bordeaux 278403 3 1962 Brest 136104 4 1962 Dijon 135694 5 1962 Grenoble 156707 6 1962 Le.Havre 187845 7 1962 Le.Mans 132181 8 1962 Lille 239955 9 1962 Lyon 535746 10 1962 Marseille 778071 On prêtera une attention particulière au type de chacune des variables. Cette ``data.frame`` est maintenant parfaitement formattée pour la construction de graphiques. Un premier graphique .................... Tracer l'évolution de la population de Bordeaux. Le graphique doit être similaire à celui ci dessous. .. > p <- ggplot(dfl %>% filter(city == "Bordeaux"), mapping=aes(x=year, y=pop, group=1)) .. > p <- p + geom_line() .. > p <- p + geom_point() .. > p .. image:: images/08-arrays-fig01.png :scale: 100 % :align: center .. tip:: Comme ``year`` est une variable catégorielle, le paramètre ``group`` doit être passé à la fonction :func:`aes` pour indiquer à :func:`geom_line` que les points doivent être reliés entre eux. Sinon, on obtient le message "Each group consists of only one observation". .. important:: ``ggplot2`` met en oeuvre un mécanisme d'héritage entre les différents layers. Les paramètres définis dans un layer de plus bas niveau sont disponibles dans un layer de plus haut niveau qui hérite des propriétés des layers inférieurs. Un paramètre hérité peut être "écrasé" dans un layer supérieur. Ainsi, le paramètre ``mapping`` peut être indifférement passé à la fonction :func:`ggplot`, ou à la fonction :func:`geom_xxx`. Modifier l'esthétique ..................... On peut modifier la façon dont les données sont affichées en passant des paramètres spécifiques aux fonctions de la famille :func:`geom_xxx`. Lire `cet article `_. .. > p <- ggplot(dfl %>% filter(city == "Bordeaux"), mapping=aes(x=year, y=pop, group=city, color=city)) .. > p <- p + geom_line(linetype="dashed") .. > p <- p + geom_point(shape=2, size=3) .. > p Passer les paramètres nécessaires à :func:`geom_line` et :func:`geom_point` pour obtenir un graphique similaire à celui ci dessous: .. image:: images/08-arrays-fig02.png :scale: 100 % :align: center Plusieurs courbes sur le même graphique ....................................... La structuration apportée par ``ggplot2`` permet de construire des graphes modulaires. Construire un graphe similaire à celui ci dessous, en modifiant le filtrage effectué sur la ``data.frame``. Définir le maximum de paramètres dans le layer de plus bas niveau avec la fonction :func:`ggplot`. .. > p <- ggplot(dfl %>% filter(city %in% c("Bordeaux", "Lille")), mapping=aes(x=year, y=pop, group=city, color=city)) .. > p <- p + geom_line(linetype="dashed") .. > p <- p + geom_point(shape=2, size=3) .. > p .. image:: images/08-arrays-fig03.png :scale: 100 % :align: center Ajuster les paramètres du graphique ................................... Pour produire un graphique de qualité, l'affichage des données brutes n'est pas suffisant, même si ``ggplot2`` construit par lui même une information pertinente, en produisant automatiquement une légende, des labels pour les axes, etc... Il est nécessaire d'ajouter un titre, des labels autres que ceux produits par défaut à partir du nom des variables, une échelle adaptée, et plein d'autres choses encore. Améliorer le graphique précédent : - en ajoutant un titre et des labels personnalisés - en fixant manuellement les limites de l'axe des abscisses .. p <- p + labs(x="Years", y="Population", title="Population variation") .. p <- p + coord_cartesian(ylim = c(0, 5e5)) .. image:: images/08-arrays-fig04.png :scale: 100 % :align: center Réduction de données .................... ``ggplot2`` utilise les données contenues dans la ``data.frame`` pour construire les graphiques, mais peut faire appel au layer ``stat`` pour effectuer une transformation statistique des données (on dit aussi réduction). Ce peut être le calcul d'une moyenne :func:`mean`, d'une somme :func:`sum`, etc... Tracer l'évolution de la population dans l'ensemble des 20 plus grandes villes de France en utilisant la fonction :func:`stat_summary`. .. p <- ggplot(dfl, mapping=aes(x=year, y=pop, group=1)) .. p <- p + stat_summary(fun = sum, geom="line", colour = "red") .. p <- p + stat_summary(fun = sum, geom="point", colour = "red", size=3) .. p <- p + labs(x="Years", y="Population (millions)", title="Population of the 20 largest cities") .. p <- p + scale_y_continuous(labels=function(x)x/1e6) .. p <- p + coord_cartesian(ylim = c(6e6, 8e6)) .. p Le graphique doit ressembler à celui ci dessous. .. image:: images/08-arrays-fig05.png :scale: 100 % :align: center Toutes les données .................. Essayons maintenant d'avoir une vue d'ensemble du jeu de données en affichant l'ensemble des données sur un seul et même graphique. Courbes superposées +++++++++++++++++++ Quelles sont les instructions permettant de superposer les courbes et d'obtenir le graphique ci dessous ? .. p <- ggplot(dfl, mapping=aes(x=year, y=pop, group=city, color=city)) .. p <- p + geom_line(linetype="dashed") .. p .. image:: images/08-arrays-fig06.png :scale: 100 % :align: center L'objectif est atteint mais le graphique est illisible. Bar plot ++++++++ Construire un barplot présentant la population des 20 plus grandes villes de France pour l'année 1962. .. > library(ggplot2) .. > p <- ggplot(dfl %>% filter(year == "1962"), mapping=aes(x=city, y=pop)) .. > p <- p + geom_bar(stat="identity") .. > p <- p + theme(axis.text.x=element_text(angle=90,hjust=1,vjust=0.5)) .. > p Le résultat doit ressembler à ça: .. image:: images/05-vectors-fig-01.png :scale: 100 % Utiliser le facetting pour présenter l'évolution de population pour chacune des villes. .. image:: images/fr-12-first_facets.png :scale: 100 % Exclure Paris, Marseille et Lyon pour présenter sur un même graphique uniquement des villes de taille à peu près comparables. .. > p <- ggplot(dfl %>% filter(!city %in% c("Paris", "Marseille", "Lyon")), mapping=aes(x=year, y=pop)) .. > p <- p + geom_bar(stat="identity") .. > p <- p + theme(axis.text.x=element_text(angle=90,hjust=1,vjust=0.5)) .. > p <- p + facet_wrap(~city, ncol=4) .. > p .. image:: images/fr-12-second_facets.png :scale: 100 % L'utilisation de la fonction :func:`bar_geom` produit une autre visualisation. .. p <- ggplot(dfl, mapping=aes(x=year, y=pop, group=city, color=city, fill=city)) .. p <- p + geom_bar(stat="identity") .. p .. image:: images/08-arrays-fig07.png :scale: 100 % :align: center Ecrire les instructions permettant d'obtenir le graphique ci dessus. Utiliser un grand nombre de paramètres sur un graphique n'est pas forcément judicieux (échelle de couleur difficile à lire, beaucoup de groupements, etc...). Le choix des villes comme paramètre n'est donc pas très pertinent. Comme les données sont également paramétrées en fonction des années, on peut également utiliser la variable ``year``. Ecrire les instructions permettant de construire le graphique ci dessous. .. > p <- ggplot(dfl, mapping=aes(x=city, y=pop, group=year, color=year, fill=year)) .. > p <- p + geom_bar(stat="identity") .. > p <- p + theme(axis.text.x=element_text(angle=90,hjust=1,vjust=0.5)) .. > p .. image:: images/08-arrays-fig08.png :scale: 100 % :align: center Ce graphique nous donne une information qualitative (les 4 villes les plus peuplées sont Paris, Marseille, Lyon et Toulouse, la population de Paris a diminué depuis les années 60, etc...). En fait le nombre important de données et de groupements (20 villes et 8 années) rend l'affichage complexe. Heat map ++++++++ Un autre choix peut être un graphique de type "heat map", pour lequel un gradient de couleur est utilisé. La fonction mise en jeu ici est :func:`geom_tile`. Ecrire les instructions pour construire le graphique ci dessous. .. > p <- ggplot(dfl, mapping=aes(x=city, y=year)) .. > p <- p + geom_tile(aes(fill = pop), color="white") .. > p <- p + theme(axis.text.x=element_text(angle=90,hjust=1,vjust=0.5)) .. > p .. image:: images/08-arrays-fig09.png :scale: 100 % :align: center Ce type de graphique est cependant plus pertinent avec des données continues, ce qui n'est pas le cas ici. La représentation de la population de Paris nettement plus importante que celle des autres villes, est un réel challenge. On pourrait penser à utiliser une fonction de compression de données, comme la fonction logarithmique par exemple, qui distribue le gradient de couleur entre des valeurs moins éloignées. Ecrire les instructions affichant le ``log`` de la population. .. p <- ggplot(dfl, mapping=aes(x=city, y=year)) .. p <- p + geom_tile(aes(fill = log(pop)), color="white") .. p <- p + theme(axis.text.x=element_text(angle=90,hjust=1,vjust=0.5)) .. p Le graphique devrait ressembler à celui ci dessous. .. image:: images/fr-12-heatmap-with-log.png :scale: 100 % :align: center Une autre idée, déjà utilisée plus haut, est de retirer les données "déviantes". Filtrer la ``data.frame`` passée à la fonction :func:`ggplot` pour obtenir le graphique ci dessous. .. p <- ggplot(dfl %>% filter(!city %in% c("Paris", "Marseille", "Lyon")), mapping=aes(x=city, y=year)) .. p <- p + geom_tile(aes(fill = pop), color="white") .. p <- p + theme(axis.text.x=element_text(angle=90,hjust=1,vjust=0.5)) .. p .. image:: images/08-arrays-fig10.png :scale: 100 % :align: center Dot plot ++++++++ Un dot plot (ou scatter plot) peut être utilisé pour représenter une variable numérique. Ecrire les instructions pour construire le graphique ci dessous. .. p <- ggplot(dfl, mapping=aes(y=city, x=pop, group=year, color=year, fill=year)) .. p <- p + geom_point() .. p .. image:: images/08-arrays-fig11.png :scale: 100 % :align: center Ce graphique donne plusieurs informations: - le nombre d'habitants "moyen" - la dispersion (évolution) au fil des recensements Quelles sont les villes dont le nombre d'habitants est resté relativement constant ? Ou au contraire ayant enregistré de grandes variations ? Quelles villes sont de taille comparable ? On peut également imaginer "normaliser" les données pour comparer l'évolution de la population relative, indépendamment de la taille de la ville:: > p <- ggplot(dfl %>% group_by(city) %>% mutate(popn=pop/max(pop)), mapping=aes(y=city, x=popn, group=year, color=year, fill=year)) > p <- p + geom_point() > p Ecrire les instructions pour construire le graphique ci dessous. On utilisera les fonctions :func:`group_by` et :func:`mutate` (dans cet ordre là) pour grouper les données de la ``data.frame`` par ville avant de les normaliser. La fonction de normalisation utilisée sera:: x / max(x) .. image:: images/fr-12-pop-norm.png :scale: 100 % :align: center Quelle est la ville dont la population est resté la plus stable ? Quelle est la ville dont la population a le plus varié ? Ressources additionnelles ------------------------- Ce chapitre présente des exemples de flux de traitements des données basés sur les packages **Tidyverse**. Il est fait une utilisation intensive des fonctions du package `dplyr `_ dont on consultera systématiquement `la référence `_. Au delà de la syntaxe des fonctions, les articles suivants recèlent une mine d'informations: - `Grouped data `_ aborde la problèmatique du groupement de données - `Two-table verbs `_ est utile pour combiner plusieurs data frames - `dplyr <-> base R `_ donne une équivalence entre les fonctions R core et leur équivalent dans l'éco système **Tidyverse**. - `Column-wise operations `_ donne des informations sur le traitement simultané de plusieurs variables - bien que R soit optimisé pour le traitement de variables, `Row-wise operations `_ fait le point sur la façon de traiter simultanément plusieurs observations. Le `Tidyverse design guide `_ donne les bonnes pratiques de l'utilisation de l'éco système. On trouvera également une information plus exhaustive pour les sujets suivants: - le temps et les dates : package `lubridate `_. Une mise en oeuvre est disponible dans le chapitre `Dates and times `_ de l'excellent `R for Data Science `__. - les chaines de caractères : package `stringr `_. Une mise en oeuvre est disponible dans le chapitre `Strings `_ de l'excellent `R for Data Science `__.