Data visualisation with and ggplot2

Author

NC

Onyxia

Check out the slides below or click here to view the slides in full screen.

In this practical session, We will learn how to create synthetic graphical representations with , which is very well equipped for this task thanks to the ggplot2 library. This library implements a grammar of graphics that is flexible, consistent, and easy to use.

If you are interested in Python , a very similar version of this practical session is available in my ENSAE course.

Note

Some code examples have annotations on the side; hover your mouse over them to display the notes, like below:

"une annotation explicative m'accompagne à droite"
1
I appear when you hover your mouse over me 🐭 !

In this course, the practice of visualization will involve replicating charts found on the Paris Open Data page here.

This practical session aims to introduce:

In this chapter, we will use the following libraries:

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

Then, we will see how to easily create maps in equivalent formats.

Note

Being able to create interesting data visualizations is an essential skill for any data scientist or researcher. To improve the quality of these visualizations, it is recommended to follow some advice given by data visualization specialists on graphical semiology.

Good data visualizations, such as those in the New York Times, rely not only on appropriate tools (JavaScript libraries) but also on certain rules of representation that allow the message of a visualization to be understood in seconds.

This blog post is a useful resource to consult regularly. This blog post by Albert Rapp demonstrates how to gradually build a good data visualization.

1 Data

A subset of Paris Open Data has been made available to facilitate import.
It is an extraction, somewhat dated, of the original dataset where only the columns used in this exercise have been retained.

We propose downloading these data and saving them to a local file before importing them1. However, we will not do this manually but rather use . Doing this manually would be bad practice in terms of reproducibility.

url <- "https://minio.lab.sspcloud.fr/projet-formation/diffusion/python-datascientist/bike.csv"
download.file(url, "bike.gz")
1
The .gz extension is important because readr needs it to understand that the file is compressed.

2 Initial graphical outputs

Trying to produce a perfect visualization on the first attempt is unrealistic. It is more realistic to gradually improve a graphic representation to highlight structural effects in a dataset step by step.

We will therefore start by representing the distribution of traffic at the main measurement stations.
To do this, we will quickly produce a barplot and then improve it gradually.

In this section, we will reproduce the first two charts from the data analysis page: The 10 counters with the highest hourly average and The 10 counters with the highest total counts. The numeric values in the plots will differ from those on the online page, which is normal because we are working with older data.

Exercise 1: Import the data and produce an initial plot

The data contains several dimensions suitable for statistical analysis. Therefore, it is necessary first to summarize them with aggregations to create a readable plot.

  1. Import the bike counter data from the bike.gz file;
  2. Keep the ten counters with the highest average;
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)
The 10 main stations after step 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. Normally, we recommend using the download URL directly to avoid creating an intermediate file on the disk. However, direct import with readr will not work here because the library does not recognize that the file is compressed without the .gz extension.↩︎

  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.↩︎