Produire des représentations graphiques avec et ggplot2

Author

Lino Galiana

Onyxia

Dérouler les slides ci-dessous ou cliquer ici pour afficher les slides en plein écran.

Dans ce troisième TP, nous allons apprendre à créer des représentations graphiques synthétiques avec qui est très bien outillé dans le domaine grâce à la librairie ggplot2. Cette dernière implémente une grammaire des graphiques flexible, cohérente et simple d’usage.

Si vous êtes intéressés par Python , une version très proche de ce TP est disponible dans mon cours de l’ENSAE.

Note

Certains exemples de code présentent des annotations sur le côté, passez votre souris dessus pour les afficher, comme ci-dessous

"une annotation explicative m'accompagne à droite"
1
Je m’affiche quand on passe la souris sur moi 🐭 !

La pratique de la visualisation se fera, dans ce cours, en répliquant des graphiques qu’on peut trouver sur la page de l’open data de la ville de Paris ici.

Ce TP vise à initier:

Dans ce chapitre, nous allons utiliser les librairies suivantes:

library(scales)
library(readr)
library(dplyr)
library(forcats)
library(lubridate)
library(ggplot2)
library(plotly)

Nous verrons par la suite la manière de construire des cartes facilement avec des formats équivalents.

Note

Être capable de construire des visualisations de données intéressantes est une compétence nécessaire à tout data-scientist ou chercheur. Pour améliorer la qualité de ces visualisations, il est recommandé de suivre certains conseils donnés par des spécialistes de la dataviz sur la sémiologie graphique.

Les bonnes visualisations de données, comme celles du New York Times, reposent certes sur des outils adaptés (des librairies JavaScript) mais aussi sur certaines règles de représentation qui permettent de comprendre en quelques secondes le message d’une visualisation.

Ce post de blog est une ressource qu’il est utile de consulter régulièrement. Ce post de blog d’Albert Rapp montre bien comment construire graduellement une bonne visualisation de données.

1 Données

Un sous-ensemble des données de Paris Open Data a été mis à disposition pour faciliter l’import. Il s’agit d’une extraction, qui commence à dater, des données disponibles sur le site où seules les colonnes qui servent à cet exercice ont été conservées.

Nous proposons de télécharger ces données et les enregistrer dans un fichier sur le disque dur local avant de l’importer1. Cependant, nous n’allons pas faire cela manuellement mais nous allons plutôt utiliser . Effectuer ce type d’action de manière manuelle serait une mauvaise pratique du point de vue de la reproductibilité.

url <- "https://minio.lab.sspcloud.fr/projet-formation/diffusion/python-datascientist/bike.csv"
download.file(url, "bike.gz")
1
L’extension .gz est importante pour la suite car readr en a besoin pour comprendre que le fichier est compressé.

2 Premières productions graphiques

Chercher à produire une visualisation parfaite du premier coup est illusoire. Il est beaucoup plus réaliste d’améliorer graduellement une représentation graphique afin, petit à petit, de mettre en avant les effets de structure dans un jeu de données.

Nous allons donc commencer par nous représenter la distribution des passages aux principales stations de mesure. Pour cela nous allons produire rapidement un barplot puis l’améliorer graduellement.

Dans cette partie, nous allons ainsi reproduire les deux premiers graphiques de la page d’analyse des données: Les 10 compteurs avec la moyenne horaire la plus élevée et Les 10 compteurs ayant comptabilisés le plus de vélos. Les valeurs chiffrées des graphiques seront différentes de celles de la page en ligne, c’est normal, nous travaillons sur des données plus anciennes.

Exercice 1 : Importer les données et produire un premier graphique rapidement

Les données comportent plusieurs dimensions pouvant faire l’objet d’une analyse statistique. Il est donc nécessaire dans un premier temps de synthétiser celles-ci par des agrégations afin d’avoir un graphique lisible.

  1. Importer les données de compteurs de vélos depuis le fichier bike.gz ;
  2. Garder les dix bornes à la moyenne la plus élevée ;
df <- readr::read_csv("bike.gz")
df1 <- df %>%
  group_by(`Nom du compteur`) %>%
  summarise(`Comptage horaire` = mean(`Comptage horaire`, na.rm = TRUE)) %>%
  arrange(desc(`Comptage horaire`)) %>%
  head(10)
Les 10 principales stations à l’issue de la question 2
head(df1)
# A tibble: 6 × 2
  `Nom du compteur`                    `Comptage horaire`
  <chr>                                             <dbl>
1 Totem 73 boulevard de Sébastopol S-N               197.
2 Totem 73 boulevard de Sébastopol N-S               148.
3 89 boulevard de Magenta NO-SE                      144.
4 Totem 64 Rue de Rivoli O-E                         140.
5 102 boulevard de Magenta SE-NO                     137.
6 72 boulevard Voltaire NO-SE                        124.

On va maintenant pouvoir se concentrer sur la production de la représentation

  1. En premier lieu, sans se préoccuper des éléments de style ni de la beauté du graphique, créer la structure du barplot (diagramme en batons) de la page d’analyse des données:
Figure obtenue, sans s’occuper du style
ggplot(df1, aes(y = `Nom du compteur`, x = `Comptage horaire`)) +
  geom_bar(stat = "identity")

La suite de l’exercice consiste à améliorer graduellement cette représentation pour converger vers la reproduction de la version en open data. Il ne s’agit pas encore de se concentrer sur l’esthétique de la figure mais de la rendre intelligible, à gros trait.

  1. En premier lieu, réordonner les barres sur l’axe des ordonnées grâce à la fonction reorder. Cela rendra le message de la figure plus intelligible.
Figure avec les barres réordonnées
figure1 <- ggplot(df1,
       aes(y = reorder(`Nom du compteur`, `Comptage horaire`),
           x = `Comptage horaire`)
       ) +
  geom_bar(stat = "identity")
figure1
1
On réordonne Nom du compteur en fonction de Comptage horaire

  1. Modifier votre couche esthétique afin d’appliquer une couleur rouge à l’ensemble des barres
Figure avec les barres rouges
figure1 <- ggplot(df1,
       aes(y = reorder(`Nom du compteur`, `Comptage horaire`),
           x = `Comptage horaire`)
       ) +
  geom_bar(stat = "identity", fill = "red")
figure1

On commence à avoir quelque chose qui commence à transmettre un message synthétique sur la nature des données. On peut néanmoins remarquer plusieurs éléments problématiques (par exemple les labels) mais aussi des éléments ne correspondant pas (les titres des axes, etc.) ou manquants (le nom du graphique…)

Exercice 2 : Un peu de style !

La figure comporte maintenant un message mais il est encore peu lisible.

  1. Le minimum pour que quelqu’un ne connaissant pas les données soit capable de comprendre la représentation graphique est de labelliser les axes. Créer les mêmes labels d’axes que la figure originale.
Figure avec les axes nommés
figure1 <- figure1 + labs(
  title = "Les 10 compteurs avec la moyenne horaire la plus élevée",
  x = "Nom du compteur",
  y = "Moyenne horaire"
)
figure1
1
Il existe de nombreuses manières de procéder avec ggplot pour labelliser les axes. Mais la plus simple est la fonction labs

  1. Le fond gris permet est certes une marque distinctive que le graphique a été produit avec ggplot2 mais ce n’est pas très soigné. Utiliser un thème plus minimaliste afin d’avoir un fond blanc.
Figure avec un fond blanc
figure1 <- figure1 +
  theme_minimal()
figure1

  1. Maintenant, concentrons nous sur les éléments plus esthétiques. Comme c’est une fonctionnalité un peu plus avancée, nous proposons directement le code pour mettre à jour votre figure avec les éléments de style suivant:
theme(
  axis.text.x = element_text(angle = 45, hjust = 1, color = "red"),
  axis.title.x = element_text(color = "red"),
  plot.title = element_text(hjust = 0.5),
  plot.margin = margin(1, 4, 1, 1, "cm")
)
Figure obtenue à l’issue de ces questions
figure1 <- figure1 +
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1, color = "red"),
    axis.title.x = element_text(color = "red"),
    plot.title = element_text(hjust = 0.5),
    plot.margin = margin(1, 4, 1, 1, "cm")
    )
figure1

  1. Enfin ajoutons de la complexité au graphique avec les chiffres sur les barres. En vous aidant de ce post, ajouter les comptages horaires moyens comme sur la figure de l’open data parisien2
figure1 <- figure1 + 
  geom_text(
    aes(label=round(`Comptage horaire`)),
    position=position_dodge(width=0.9),
    hjust=-0.5
    )

On comprend ainsi que le boulevard de Sébastopol est le plus emprunté, ce qui ne vous suprendra pas si vous faites du vélo à Paris. Néanmoins, si vous n’êtes pas familiers avec la géographie parisienne, cela sera peu informatif pour vous, vous allez avoir besoin d’une représentation graphique supplémentaire: une carte ! Nous verrons ceci lors d’un prochain chapitre.

figure1

Les 10 compteurs avec la moyenne horaire la plus élevée
Exercice 3: produire une nouvelle figure

Faire la même chose pour la figure 2 (“Les 10 compteurs ayant comptabilisés le plus de vélos”), afin d’obtenir une figure similaire.

df2 <- df %>%
  group_by(`Nom du compteur`) %>%
  summarise(`Comptage horaire` = sum(`Comptage horaire`, na.rm = TRUE)) %>%
  arrange(desc(`Comptage horaire`)) %>%
  head(10)
# Create a horizontal bar plot
figure2 <- ggplot(df2, aes(y = reorder(`Nom du compteur`, `Comptage horaire`), x = `Comptage horaire`)) +
  geom_bar(stat = "identity", fill = "forestgreen") +
  labs(title = "Les 10 compteurs ayant comptabilisés le plus de vélos",
       x = "Nom du compteur",
       y = "La somme des vélos comptabilisés sur la période sélectionnée") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1),
        axis.title.x = element_text(color = "forestgreen"),
        plot.title = element_text(hjust = 0.5),
        plot.margin = margin(1, 4, 1, 1, "cm"))
Figure 2 à l’issue de cet exercice
figure2

Les diagrammes en batons (barplot) sont extrêmement communs mais qu’ils transmettent. Sur le plan sémiologique, les lollipop charts sont préférables: ils transmettent la même information mais avec moins de bruit (la largeur des barres du barplot noie un peu l’information).

Voici, par exemple, la deuxième figure de la page, rendue non plus sous forme de barplot mais sous forme de lollipop chart:

df2_lollipop <- df2 %>%
  mutate(x =  fct_reorder(`Nom du compteur`, `Comptage horaire` ), y = `Comptage horaire`)

figure2_lollipop <- ggplot(df2_lollipop, aes(x=x, y=y)) +
    geom_segment( aes(xend=x, yend=0), alpha = 0.4) +
    geom_point( size=5, color="forestgreen") +
    coord_flip() +
  labs(title = "Les 10 compteurs ayant comptabilisés le plus de vélos",
       x = "Nom du compteur",
       y = "La somme des vélos comptabilisés sur la période sélectionnée") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1),
        axis.title.x = element_text(color = "forestgreen"),
        plot.title = element_text(hjust = 0.5),
        plot.margin = margin(1, 4, 1, 1, "cm")) +
  scale_y_continuous(labels = unit_format(unit = "M", scale=1e-6))
figure2_lollipop

Comparaison du barplot et du lollipop chart
figure2
figure2_lollipop

Barplot

Lollipop

Choisissez votre représentation préférée

Exercice 3 bis (optionnel): produire un lollipop chart

Reprendre l’exercice 2 mais à la place d’un barplot, produire un lollipop chart.

3 Première agrégation temporelle

On va maintenant se concentrer sur la dimension spatiale de notre jeu de données à travers deux approches:

  • Un diagramme en barre synthétisant l’information de notre jeu de données de manière mensuelle ;
  • Des séries instructives sur la dynamique temporelle. Cela sera l’objet de la prochaine partie ;

Pour commencer, reproduisons la troisième figure qui est, encore une fois, un barplot. La première question implique une première rencontre avec une donnée temporelle à travers une opération assez classique en séries temporelles: changer le format d’une date pour pouvoir faire une agrégation à un pas de temps plus large.

Exercice 4: barplot des comptages mensuels
  1. Utiliser la fonction format pour créer une variable month dont le format respecte, par exemple, le schéma 2019-08 ;
  2. Faire la moyenne des comptages horaires pour chaque mois
df <- df %>%
  mutate(month = format(`Date et heure de comptage`, "%Y-%m"))
# Question 2
comptage_horaires_mois <- df %>%
  group_by(month) %>%
  summarise(value = mean(`Comptage horaire`, na.rm = TRUE))
Comptages horaires obtenus à l’issue de cette question
comptage_horaires_mois
# A tibble: 14 × 2
   month   value
   <chr>   <dbl>
 1 2019-08  33.6
 2 2019-09  55.8
 3 2019-10  49.9
 4 2019-11  36.0
 5 2019-12  67.9
 6 2020-01  66.1
 7 2020-02  43.2
 8 2020-03  29.4
 9 2020-04  12.5
10 2020-05  54.6
11 2020-06  85.0
12 2020-07  80.7
13 2020-08  53.2
14 2020-09  98.3

Appliquer les conseils précédents pour construire et améliorer graduellement un graphique afin d’obtenir une figure similaire à la 3e production sur la page de l’open data parisien.

figure3 <- ggplot(comptage_horaires_mois) +
  geom_bar(aes(x = month, y = value), fill = "#ffcd00", stat = "identity") +
  labs(x = "Date et heure de comptage", y = "Moyenne mensuelle du comptage par heure\nsur la période sélectionnée",
       title = "Moyenne mensuelle des comptages vélos") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1),
        axis.title.y = element_text(color = "#ffcd00", face = "bold"),
        plot.title = element_text(hjust = 0.5),
        plot.margin = margin(1, 4, 1, 1, "cm"))
Exemple de figure reproduisant l’open data parisien
figure3

  • Question optionnelle: représenter la même information sous forme de lollipop
figure3

Si vous préférez représenter cela sous forme de lollipop3:

ggplot(comptage_horaires_mois, aes(x = month, y = value)) +
  geom_segment(aes(xend = month, yend = 0)) +
  geom_point( color="#ffcd00", size=4) +
  labs(x = "Date et heure de comptage", y = "Moyenne mensuelle du comptage par heure\nsur la période sélectionnée",
       title = "Moyenne mensuelle des comptages vélos") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1),
        #axis.title.y = element_text(color = "#ffcd00", face = "bold"),
        plot.title = element_text(hjust = 0.5),
        plot.margin = margin(1, 4, 1, 1, "cm"))

4 Première série temporelle

Il est plus commun de représenter sous forme de série les données ayant une dimension temporelle.

Exercice 5: Représenter une série temporelle
  1. Créer une variable day qui transforme l’horodatage en format journalier du type 2021-05-01.
df <- df %>%
  mutate(day = date(`Date et heure de comptage`))

moyenne_quotidienne <- df %>%
  group_by(day) %>%
  summarise(value = mean(`Comptage horaire`, na.rm = TRUE))
  1. Représenter sous forme de série temporelle cette information, sans vous préoccuper du style de la figure.
figure4 <- ggplot(moyenne_quotidienne, aes(x = day, y = value)) +
  geom_line(color = "magenta")
Figure minimaliste
figure4

  1. Colorer la zone sous la ligne avec la fonction appropriée
figure4 <- figure4 +
  geom_area(fill="magenta", alpha = 0.6)
Figure avec la coloration sous la ligne
figure4

  1. Finaliser le graphique pour reproduire la figure de la page d’open data
figure4 <- figure4 +
  labs(x = "Date et heure de comptage (Jour)", y = "Moyenne journalière du comptage par heure\nsur la période sélectionnée",
       title = "Moyenne journalière des comptages vélos") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1),
        plot.title = element_text(hjust = 0.5),
        plot.margin = margin(1, 4, 1, 1, "cm"))
Figure finalisée
figure4

Voici quelques aides pour cet exercice

💡 Aide pour la question 1 Regardez la fonction day du package lubridate
💡 Aide pour la question 3 Ce thread sur stackoverflow peut vous aider.
Le jeu de données pour la figure 4
head(moyenne_quotidienne)
# A tibble: 6 × 2
  day        value
  <date>     <dbl>
1 2019-08-01  46.7
2 2019-08-02  40.0
3 2019-08-03  30.7
4 2019-08-04  28.9
5 2019-08-05  36.0
6 2019-08-06  29.9

A l’issue de cet exercice, on obtient ainsi une figure prenant la forme suivante:

figure4

5 Introduction aux graphiques HTML avec Plotly

L’inconvénient des figures avec ggplot est que celles-ci ne permettent pas d’interaction avec le lecteur. Toute l’information doit donc être contenue dans la figure ce qui peut la rendre difficile à lire. Si la figure est bien faite, avec différents niveaux d’information, cela peut bien fonctionner.

Il est néanmoins plus simple, grâce aux technologies web, de proposer des visualisations à plusieurs niveaux. Un premier niveau d’information, celui du coup d’oeil, peut suffire à assimiler les principaux messages de la visualisation. Ensuite, un comportement plus volontaire de recherche d’information secondaire peut permettre d’en savoir plus. Les visualisations réactives, qui sont maintenant la norme dans le monde de la dataviz, permettent ce type d’approche: le lecteur d’une visualisation peut passer sa souris à la recherche d’information complémentaire (par exemple les valeurs exactes) ou cliquer pour faire apparaître des informations complémentaires sur la visualisation ou autour.

Ces visualisations reposent sur le même triptyque que l’ensemble de l’écosystème web: HTML, CSS et JavaScript. Les utilisateurs de ne vont jamais manipuler directement ces langages, qui demandent une certaine expertise, mais vont utiliser des librairies au niveau de qui génèreront automatiquement tout le code HTML, CSS et JavaScript permettant de créer la figure.

Exercice 6: série temporelle interactive
  1. Créer une figure Plotly basique pour représenter sous forme de série temporelle la figure 4, sans se préoccuper du style
Série temporelle produite sans élément de style
plot_ly(
  moyenne_quotidienne, x = ~day, y = ~value,
  type = 'scatter', mode = 'lines'
)
  1. A partir de l’exemple dans la documentation, ajouter une aire sous la figure
Ajout de la couche sous la ligne
plot_ly(
  moyenne_quotidienne, x = ~day, y = ~value,
  fill = 'tozeroy',
  type = 'scatter', mode = 'lines'
)
  1. Jouer sur les éléments de style pour reproduire la figure 4. Pour profiter de la réactivité du graphique, soigner l’information obtenue en passant la souris sur la figure grâce aux arguments hovertemplate et hoverinfo
fig <- plot_ly(
  moyenne_quotidienne, x = ~day, y = ~value,
  color = I("magenta"),
  hovertemplate = ~paste(day, ": ", round(value), " passages de vélo en moyenne par heure"),
  hoverinfo = "text",
  fill = 'tozeroy',
  type = 'scatter', mode = 'lines')
fig4 <- fig %>%
  layout(title = "Moyenne journalière des comptages vélos",
         xaxis = list(title = "Date et heure de comptage (Jour)"),
         yaxis = list(title = "Moyenne journalière du comptage par heure\nsur la période sélectionnée"))
Figure obtenue
fig4

La version réactive de la figure est ainsi

fig4

Cette représentation montre bien le caractère spécial de l’année 2020. Pour rappeller au lecteur distrait la nature particulière de la période, marquée par un premier confinement qu’on voit bien dans les données, on peut, avec l’aide de la documentation, ajouter deux barres verticales pour marquer les dates de début et de fin de cette période:

vline <- function(x = 0, color = "royalblue") {
  list(
    type = "line",
    y0 = 0,
    y1 = 1,
    yref = "paper",
    x0 = x,
    x1 = x,
    line = list(color = color, dash="dot")
  )
}

fig4 %>% layout(shapes = list(vline("2020-03-17"), vline("2020-05-11")))

Comme dernier exercice, voici comment reproduire cette figure avec Plotly:

df1 <- df1 %>% mutate(`Nom du compteur` = fct_reorder(`Nom du compteur`, `Comptage horaire`))

fig <- plot_ly(
  df1,
  x = ~ `Comptage horaire`, y = ~`Nom du compteur`,
  color = I("red"),
  hovertext = ~paste0(`Nom du compteur`, ": ", round(`Comptage horaire`)),
  hoverinfo = 'text',
  type = 'bar',
  name = 'Principales stations')


fig <- fig %>% layout(
  yaxis = list(title = 'Moyenne horaire'),
  xaxis = list(title = 'Nom du compteur', color = "red")
  )
fig
Exercice 7: un barplot avec Plotly
  1. Pour avoir immédiatement des barres bien ordonnées, utiliser la fonction fct_reorder du package forcats pour réoordonner les valeurs du dataframe issu de l’exercice 1
  2. Utiliser Plotly pour créer votre figure.
  3. (Optionnel, plus avancé) Faire un lollipop chart avec Plotly

Footnotes

  1. D’habitude, nous recommandons d’utiliser directement l’URL de téléchargement ce qui évite de créer un fichier intermédiaire sur le disque dur. Néanmoins, ici, l’import direct avec readr ne fonctionnera pas car le fichier est mal interprété par la librairie. Celle-ci ne comprend pas que le fichier est compressé car il lui manque l’extension .gz (un format compressé) à la fin.↩︎

  2. Ce n’est pas forcément une bonne pratique de dataviz de faire cela. En effet, cela signifie que l’échelle et la diversité des données dans celle-ci ne sont pas directement intelligibles.↩︎

  3. J’ai retiré la couleur sur l’axe des ordonnées qui, je trouve, apporte peu à la figure voire dégrade la compréhension du message.↩︎