tp ggplot2 - rochevin/formation_ggplot2_2023 GitHub Wiki

library(tidyverse)
library(palmerpenguins) # des MANCHOTS, pas des pingouins !

Premier graphique

Au début, il existe mais il ne montre rien…

MonPremierGraphique <- ggplot(data=penguins)
MonPremierGraphique

Je ne vois rien ! Pourtant l’objet MonPremierGraphique existe et il contient plein de choses.

summary(MonPremierGraphique)
data: species, island, bill_length_mm, bill_depth_mm,
  flipper_length_mm, body_mass_g, sex, year [344x8]
faceting: <ggproto object: Class FacetNull, Facet, gg>
    compute_layout: function
    draw_back: function
    draw_front: function
    draw_labels: function
    draw_panels: function
    finish_data: function
    init_scales: function
    map_data: function
    params: list
    setup_data: function
    setup_params: function
    shrink: TRUE
    train_scales: function
    vars: function
    super:  <ggproto object: Class FacetNull, Facet, gg>
typeof(MonPremierGraphique)
[1] "list"
names(MonPremierGraphique)
[1] "data"        "layers"      "scales"      "mapping"     "theme"      
[6] "coordinates" "facet"       "plot_env"    "labels"     
class(MonPremierGraphique)
[1] "gg"     "ggplot"
head(MonPremierGraphique$data)
# A tibble: 6 × 8
  species island    bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
  <fct>   <fct>              <dbl>         <dbl>             <int>       <int>
1 Adelie  Torgersen           39.1          18.7               181        3750
2 Adelie  Torgersen           39.5          17.4               186        3800
3 Adelie  Torgersen           40.3          18                 195        3250
4 Adelie  Torgersen           NA            NA                  NA          NA
5 Adelie  Torgersen           36.7          19.3               193        3450
6 Adelie  Torgersen           39.3          20.6               190        3650
# ℹ 2 more variables: sex <fct>, year <int>
MonPremierGraphique$layers
list()

HW a dit : The plot is not ready to be displayed until one layer is added.

OK, donc c’est normal si je ne vois rien, car le composant layers de l’objet MonPremierGraphique ne contient rien.

… alors j’essaie d’y mettre des choses…

MonPremierGraphique <- ggplot(data = penguins,
                              aes(x = bill_length_mm,
                                  y = bill_depth_mm))
MonPremierGraphique

C’est mieux, je vois des axes avec les variables que je veux représenter en x et y, mais rien de plus.

HW a dit : A minimal layer may do nothing more than specify a geom, a way of visually representing the data.

… et finalement, je vois !

Alors, je vais ajouter un layer. Pour cela, plein de fonctions existent pour dire quels objets géométriques je veux utiliser pour la représentation. Soyons modestes, commençons par des points avec la fonction geom_point().

MonPremierGraphique + geom_point()

Youpi, trompettes et confettis ! Voilà, mon premier graphique réalisé avec ggplot2.

Sauvons-le et auscultons-le.

MonPremierGraphiqueLeVrai <- MonPremierGraphique + geom_point()
summary(MonPremierGraphiqueLeVrai)
data: species, island, bill_length_mm, bill_depth_mm,
  flipper_length_mm, body_mass_g, sex, year [344x8]
mapping:  x = ~bill_length_mm, y = ~bill_depth_mm
faceting: <ggproto object: Class FacetNull, Facet, gg>
    compute_layout: function
    draw_back: function
    draw_front: function
    draw_labels: function
    draw_panels: function
    finish_data: function
    init_scales: function
    map_data: function
    params: list
    setup_data: function
    setup_params: function
    shrink: TRUE
    train_scales: function
    vars: function
    super:  <ggproto object: Class FacetNull, Facet, gg>
-----------------------------------
geom_point: na.rm = FALSE
stat_identity: na.rm = FALSE
position_identity 
MonPremierGraphiqueLeVrai$layers
[[1]]
geom_point: na.rm = FALSE
stat_identity: na.rm = FALSE
position_identity 

Il a bien un layer et c’est pourquoi je peux voir quelque chose.

Pour mieux (?) comprendre ce qui se passe

Reprenons l’objet MonPremierGraphique

MonPremierGraphique

On sait qu’il n’affiche rien car il n’a pas de layer. On a ajouté un layer avec la fonction geom_point().

MonPremierGraphique + geom_point()

Mais on aurait aussi bien pu le faire avec la fonction layer(), fonction dans laquelle on spécifie tous les attributs d’un layer : mapping, data (ici c’est inutile car on les a déjà spécifiés dans l’objet MonPremierGraphique, geom, stat et position.

MonPremierGraphique +
  layer(
    mapping = NULL,
    data = NULL,
    geom = "point",
    stat = "identity",
    position = "identity"
)

En fait, la fonction geom_point() nous simplifie la vie en appelant elle-même la fonction layer() avec les paramètres qu’il faut.

A noter qu’il existe une façon de faire la même chose en cachant encore plus les choses : la fonction qplot().

qplot(bill_length_mm, bill_depth_mm, data = penguins)

Mais, dans la préface à la 2eme édition de ggplot: Elegant Graphics for Data Analysis

HW a dit : Switched from qplot() to ggplot() in the introduction, Chapter 2. Feedback indicated that qplot() was a crutch: it makes simple plots a little easier, but it doesn’t help with mastering the grammar.

Et effectivement, en utilisant qplot(), on garde à peu près nos habitudes liées à plot() mais la layered grammar est complètement cachée.

Bilan

Pour faire un graphique :

  • les données sont stockées dans un data.frame et pas autrement, ce n’est pas négociable !

  • les aesthetics mappings lient des variables du jeu de données à des propriétés visuelles (utiliser la fonction aes()).

  • seul l’ajout d’un layer permet de visualiser quelque chose (utiliser les fonctions geom_*()).

  • Ce qui donne une syntaxe comme celle-ci : ggplot(data = penguins, aes(x = bill_length_mm, y = bill_depth_mm)) + geom_point()

On est bon ? Oui ? alors, on continue…

Améliorons un peu ce premier graphique

Un peu de couleur

J’aimerais bien que les points soient colorés en fonction de la variable species du jeu de données penguins.

Il suffit pour cela de préciser un lien entre cette variable du jeu de données et la propriété visuelle colour au moyen de la fonction aes().

MonPremierGraphiqueLeVraiEnCouleur <-
  MonPremierGraphiqueLeVrai + aes(colour = species)
MonPremierGraphiqueLeVraiEnCouleur

En changeant la forme

En fait, les couleurs c’est bien, mais si mes lecteurs les distinguent mal, je préfère changer la forme (shape) plutôt que la couleur.

MonPremierGraphiqueLeVraiEnFormes <-
  MonPremierGraphiqueLeVrai + aes(shape = species)
MonPremierGraphiqueLeVraiEnFormes

Mouais, pas très lisible. Et si je combinais les deux…

Un peu de couleur… en changeant la forme

On peut utiliser la même variable pour donner à la fois la couleur et la forme. Dans ce cas, une seule légende est affichée.

MonPremierGraphiqueLeVraiEnCouleurEtFormes <- ggplot(data = penguins,
                            aes(x = bill_length_mm, y = bill_depth_mm,
                                colour = species, shape = species)) +
                            geom_point()
MonPremierGraphiqueLeVraiEnCouleurEtFormes

Les noms des graphiques deviennnent un peu long, je vais me calmer un peu et reprendre depuis le début pour voir si j’ai compris. Je vais pousser un peu le zèle jusqu’à utiliser 2 variables qualitatives (1 pour la couleur, l’autre pour la forme), mais je suis confiant.

PointCouleurForme <- ggplot(data = penguins,
                            aes(x = bill_length_mm, y = flipper_length_mm,
                                colour = species,
                                shape = island)) + geom_point()
PointCouleurForme

Et bien voilà ! C’est pas mal tout ça.

Juste pour voir, si j’inverse les 2 variables qualitatives.

PointCouleurForme <- ggplot(data = penguins,
                            aes(x = bill_length_mm, y = flipper_length_mm,
                                colour = island,
                                shape = species)) + geom_point()
PointCouleurForme

L’impression est différente. A méditer…

Et si je m’amusais avec les autres variables

ggplot(data = penguins ,aes(x = flipper_length_mm, y = bill_length_mm,
                                colour = bill_depth_mm,
                                size = body_mass_g)) + geom_point()

Ouh là, là… c’est joli (ou pas), mais qu’ai-je donc fait ? Reprenons calmement :

  • je suis toujours dans un nuage de points (geom_point()) avec flipper_length_mm en abscisses (aes(x= )) et bill_length_mm en ordonnées (aes(y= )).

  • dans la fonction aes(), j’ai lié la couleur à la variable bill_depth_mm obtenant ainsi une dégradé de bleu (plus la valeur de bill_depth_mm est élevée, plus le symbole est clair).

  • toujours dans la fonction aes(), j’ai lié la taille à la variable body_mass_g obtenant ainsi des points d’autant plus grands que la valeur de body_mass_g est élevée.

  • pour le même prix, j’ai obtenu les deux légendes associées.

  • tous ces éléments illustrent la notion de scaling qui contrôle le lien entre les données et un attribut aesthetic. Tout attribut aesthetic a besoin d’une échelle. Parfois (souvent), on ne s’en rend pas compte parce qu’une valeur par défaut est prévue.

En vue éclatée

Ok, je peux m’amuser à personnaliser mon graphique avec des couleurs, des formes, des tailles différentes. Et si je restais sobre en présentant un graphique par modalité de la variable species.

Très facile (comme le reste !), il suffit d’utiliser le facetting.

Repartons de MonPremierGraphiqueLeVrai et utilisons, simplement, la fonction facet_wrap().

MonPremierGraphiqueLeVrai + facet_wrap(~species)

Il va de soi que l’on peut combiner tout ça.

MonPremierGraphiqueLeVrai +
  aes(colour = bill_depth_mm) +
  facet_wrap(~species)

Avec un deuxième facteur, on peut réaliser (très facilement, on s’en doute) toute une planche de graphiques, cette fois-ci avec facet_grid().

MonPremierGraphiqueLeVrai + facet_grid(species ~ island)

MonPremierGraphiqueLeVrai + facet_grid(island ~ species)

MonPremierGraphiqueLeVrai + facet_grid(species ~ sex)

C’est bien mais il n’y a pas que les nuages de points dans la vie

Histogramme

Pour réaliser un histogramme, il suffit… de le demander. On utilise toujours le data.frame penguins avec cette fois-ci seulement une variable (ici flipper_length_mm en x).

Histogramme <- ggplot(penguins, aes(x = flipper_length_mm)) +
  geom_histogram()
Histogramme

OK, pas génial, mais on a bien un histogramme. Histogramme sur lequel, on peut appliquer les recettes précédentes.

  • Le facetting pour avoir un histogramme par modalité du facteur species
Histogramme + facet_wrap(~species)

  • et aussi des couleurs (même si l’interprétation n’en est pas forcément améliorée)
Histogramme + aes(colour = species)

Histogramme + aes(fill = species)

Au fait, vous avez vu qu’il nous embête avec un warning qui parle de stat_bin().

HW a dit : The choice of bins is very important for histograms […]. In ggplot2, a very simple heuristic is used for the default number of bins: it uses 30, regardless of the data. This is perverse, and ignores all of the research on selecting good bin sizes automatically, but sends a clear message to the user that they need to think about and experiment with the bin width.

Et bim !

Alors, on va essayer de penser un peu et diminuer la largeur des intervalles (binwidth).

ggplot(penguins, aes(x = bill_length_mm)) +
  geom_histogram(binwidth = 0.5)

OK, ça a l’air d’aller mieux. Et, juste manière, avec des couleurs.

ggplot(penguins, aes(x = bill_length_mm, fill = species)) +
  geom_histogram(binwidth = 0.5)

Au fait, juste pour voir si on a compris le principe, comparons

ggplot(penguins, aes(x = bill_length_mm)) + geom_histogram()

ggplot(penguins, aes(x = bill_length_mm)) + geom_bar(stat = "bin")

C’est la même chose ! Logique non ? Un histogramme n’est rien d’autre qu’un diagramme en bâtons prenant comme données le nombre de valeurs dans chaque bin.

On peut ainsi s’amuser avec d’autres représentations graphiques de ces counts.

ggplot(penguins, aes(x = bill_length_mm)) + geom_point(stat = "bin")

ggplot(penguins, aes(x = bill_length_mm)) + geom_line(stat = "bin")

Diagramme en bâtons

Puisque que je viens de voir passer geom_bar, je vais m’attarder un peu dessus pour représenter 2 variables qualitatives.

table(penguins$island, penguins$species)
            Adelie Chinstrap Gentoo
  Biscoe        44         0    124
  Dream         56        68      0
  Torgersen     52         0      0
ggplot(penguins, aes(x=species, fill=island)) + geom_bar()

ggplot(penguins, aes(x=species, fill=island)) + geom_bar(position = "dodge")

ggplot(penguins, aes(x=species, fill=island)) + geom_bar(position = "fill")

ggplot(penguins, aes(x=species, fill=sex)) + geom_bar(position = "dodge")

Densité

Je poursuis autour de la distribution des données avec la représentation de densité grâce à la fonction… devinez… geom_density().

Pour démarrer, c’est facile, maintenant on gère ça tranquillement.

ggplot(penguins, aes(x = bill_length_mm)) + geom_density()

Je vais le faire aussi en couleur par modalité de species, maintenant que je suis à l’aise.

ggplot(penguins, aes(x = bill_length_mm, colour = species)) +
  geom_density()

Pour avoir des lignes dans la légende on doit passer par stat_density() :

ggplot(penguins, aes(x = bill_length_mm, colour = species)) +
 stat_density(geom="line", position = "identity")

Au fait, on a vu fill tout à l’heure, regardons ce que ça donne ici

ggplot(penguins, aes(x = bill_length_mm, fill = species)) +
  geom_density()

Superbe ! Euh oui, sauf que je m’attendais à mieux en fait, mais je ne sais pas trop quoi ? Que les courbes ne se cachent les unes derrière les autres par exemple ? Oui c’est exactement ça ! Et alors, on fait comment ? On ajoute de la transparence avec le paramètre alpha.

ggplot(penguins, aes(x = bill_length_mm, fill = species)) +
  geom_density(alpha = 0.4)

Ah ouais, là chapeau. Je prends.

Boxplot

Poursuivons avec des boxplots. D’abord un seul

MonGraphe <- ggplot(penguins)
MonGraphe + geom_boxplot(aes(y = bill_length_mm))

puis par modalité de species encore une fois. Pour cela, il suffit de préciser que l’axe des x du graphique est lié à la variable species (oui, elle est qualitative, et alors ?).

MonGraphe + geom_boxplot(aes(x = species, y = bill_length_mm))

Et si je prefère des boxplots à l’horizontale, il suffit d’échanger les coordonnées

MonGraphe + geom_boxplot(aes(y = species, x = bill_length_mm))

ou d’utiliser la fonction coord_flip().

MonGraphe + geom_boxplot(aes(x = species, y = bill_length_mm)) +
  coord_flip()

Ce qui est bien avec les boxplots c’est quand on peut aussi voir les points. Simple, il suffit d’ajouter un layer au graphique des boxplots.

MonGraphe +
  geom_boxplot(aes(x = species, y = bill_length_mm)) +
  geom_point(aes(x = species, y = bill_length_mm))

Euh oui d’accord, mais c’est pas terrible, on peut pas faire mieux ? Je reformule : comment peut-on faire mieux ?

En évitant la superposition des points par exemple et en jouant aussi sur la tranparence.

MonGraphe +
  geom_boxplot(aes(x = species, y = bill_length_mm)) +
  geom_point(aes(x = species, y = bill_length_mm),
             alpha = 0.2, position = "jitter")

J’ai écrit deux fois la même chose dans ma commande (aes(x = species, y = bill_length_mm)), ne pouvais-je pas m’en passer ? Si bien sûr, comme pour les calculs, on factorise. Reprenons depuis le début.

ggplot(penguins, aes(x = species, y = bill_length_mm)) +
  geom_boxplot() +
  geom_point(alpha = 0.2, position = "jitter")

Le fait d’utiliser species en x et bill_length_mm en y étant commun aux deux graphiques que je veux réaliser, je positionne ces informations à un niveau supérieur.

Mais je ne suis pas obligé de tout remonter, il peut y avoir des aesthetics propres à un layer comme ici où on utilise la variable bill_depth_mm pour la taille des points (et cela n’a rien à voir avec les boxplots).

ggplot(penguins, aes(x = species, y = bill_length_mm)) +
  geom_boxplot() +
  geom_point(aes(size = bill_depth_mm),
             alpha = 0.2, position = "jitter")

Sinon, il y aussi la fonction geom_jitter() qui fait le boulot aussi bien (mieux ?) et on peut même resserrer un peu les points.

ggplot(penguins, aes(x = species, y = bill_length_mm)) +
  geom_boxplot() +
  geom_jitter(alpha = 0.2, width = 0.1)

Jouons du violon

Et si je remplaçais les boîtes par des violons ! Il suffit de remplacer geom_boxplot par… geom_violin.

ggplot(penguins, aes(x = species, y = bill_length_mm)) +
  geom_violin() +
  geom_point(aes(size = bill_length_mm),
             alpha = 0.2, position = "jitter")

Pour les mêmes en couleur, avec un peu de transparence sur les violons :

ggplot(penguins, aes(x = species, y = bill_length_mm)) +
  geom_violin(aes(fill = species), alpha = 0.2) +
  geom_jitter(aes(size =  bill_length_mm, colour = species),
             width = 0.2, alpha = 0.6)

C’est presque trop facile.

Les nuages de points : le retour

Au cas où vous ne seriez pas encore convaincus par ggplot2, on va revenir un moment sur les nuages de points. Et là, normalement, c’est l’effet waouh garanti.

On a vu quelque part qu’un layer a 5 parties : 1/ les données 2/ les aesthetics, 3/ les geoms, 4/ les transformations statistiques et 5/ les ajustements de position. Jusqu’à présent, on a vu un peu de tout ça, sauf des statistiques, non ? Alors, allons-y !

MonNuage <- ggplot(penguins, aes(x = flipper_length_mm, y = bill_length_mm)) +
  geom_point()
MonNuage

OK, jusqu’ici, on connaît, pas de quoi s’enflammer. Attention, le spectacle va commencer.

MonNuage + geom_smooth()

Euh oui d’accord, c’est bien mais il sort d’où ce truc ? Et bien, il nous l’a dit geom_smooth() using method = 'loess' and formula 'y ~ x'.

Un peu trop lisse ?

MonNuage + geom_smooth(span = 0.2)

Pas assez ?

MonNuage + geom_smooth(span = 1.2)

OK, si vous préférez une régression linéaire plutôt qu’un loess, il suffit de demander.

MonNuage + geom_smooth(method = "lm")

Et d’autres, voir ?geom_smooth.

Déclinons là-dessus ce que l’on sait faire.

  • En facettes :
MonNuage + geom_smooth() + facet_wrap(~species)

MonNuage + geom_smooth() + facet_grid(species~island)

MonNuage + geom_smooth() + facet_wrap(species~island)

On note que l’échelle en x et en y est la même pour tous les sous-graphiques. On peut “libérer” l’une et/ou l’autre de ces échelles

MonNuage + geom_smooth() + facet_wrap(~species, scale = "free")

MonNuage + geom_smooth() + facet_wrap(~species, scale = "free_x")

MonNuage + geom_smooth() + facet_wrap(~species, scale = "free_y")

  • En couleurs
MonNuage + aes(colour = species) + geom_smooth()

Ca commence à être sympa non ? Et noter l’aspect synthétique et concis du code.

Remarque: On peut faire de l’extrapolation (seulement avec la méthode lm):

MonNuage + aes(colour = species) + geom_smooth(method = "lm")

MonNuage + aes(colour = species) + geom_smooth(method = "lm", fullrange = TRUE)

Un petit truc pour parler des systèmes de coordonnées ou comment réaliser un camembert ?

On peut apprécier ou pas les diagrammes circulaires ou camembert ou pie chart, mais ce serait bien de savoir en faire avec ggplot2. Cherchez bien dans l’aide, vous ne trouverez pas de fonction geom_pie().

Alors que R de base le fait très simplement.

pie(table(penguins$species))

Et là, on ne serait pas un peu déçu de ne pas avoir de légende automatiquement ? C’est qu’on y prend goût à ggplot2.

Au fait, si ces graphiques ne sont pas trop appréciés, c’est parce qu’on leur reproche le fait de devoir interpréter des angles ce que l’oeil humain ne parvient pas forcément très bien à faire. Et c’est pourquoi, on peut lui préférer un simple diagramme en bâtons, que R de base fait aussi très bien.

barplot(table(penguins$species))

Ouais, c’est quand même un peu tristounet. Mais les diagrammes en bâtons, je sais faire avec ggplot2

ggplot(penguins, aes(x = species)) + geom_bar()

Et là, révélation ! Un diagramme circulaire, c’est un diagramme en bâtons ! euh, non… Mais si, mais dans un repère de coordonnées polaires ! Ah, oui, euh, peut-être… [Sceptique].

HW a dit : In the grammar, a pie chart is a stacked bar geom drawn in a polar coordinate system.

ggplot(penguins, aes(x = species)) + geom_bar() + coord_polar()

Ah oui ! Bizarre non ? OK, on arrange un peu tout ça.

ggplot(penguins, aes(x = species, fill = species)) +
  geom_bar(width = 1) +
  coord_polar()

Tada !

En fait, c’était juste pour parler de systèmes de coordonnées…

Et si je veux changer de style

Choisis ton thème ! Voir help(theme_bw).

MonNuageTest <- MonNuage + aes(colour = species) + geom_smooth() 
MonNuageTest + theme_bw()

MonNuageTest + theme_dark()

MonNuageTest + theme_light()

MonNuageTest + theme_gray()

Jouons maintenant avec les échelles

Pour jouer avec les échelles, il y a de la matière ! Il suffit de regarder le nombre de fonctions scale_*(). Et comme on l’a vu plus tôt, chaque aesthetic a une échelle.

On pense d’abord aux échelles des axes

Les échelles linéaires

Reprenons le graphique MonNuage. Et (re-)constatons qu’il suffit de saisir son nom pour qu’il apparaisse.

MonNuage

Allons bricoler l’axe des abscisses. En parcourant la liste des fonctions scale_*(), on peut deviner (non ?) que la fonction à manipuler est scale_x_continuous() et en consultant sa fiche d’aide, on peut aisément voir comment modifier le titre (name), le découpage (breaks), les limites (limits) et d’autres choses…

MonNuage + scale_x_continuous(name = "Longueur de nageoire (mm)",
                              breaks = seq(170, 230, by = 10),
                              limits = c(170, 230))

Question : comment modifier l’axe des ordonnées ? Il serait quasiment insultant de fournir ici une réponse à cette question.

Comment zoomer ou changer les axes dans ggplot2

Important !!

Pour zoomer, toujours utiliser coord_cartesian, et surtout pas xlim, ylim, ou scale_*

p_boxplot <- ggplot(penguins, aes(x = species, y = bill_length_mm)) +
  geom_boxplot() +
  geom_point(aes(size = bill_depth_mm),
             alpha = 0.2, position = "jitter")

p_boxplot_1 <- p_boxplot + ylim(c(40,50))
p_boxplot_2 <- p_boxplot +
  coord_cartesian(ylim=c(40,50))

Les échelles log

MonNuage + aes(x = flipper_length_mm^2, y = bill_length_mm^3) +
  scale_x_log10() + scale_y_log10()

Mais il y a aussi les légendes (?!)

Pour des couleurs liées à une variable qualitative

On va ajouter un aesthetics à notre graphique pour continuer à explorer les échelles.

MonNuageCouleur <- MonNuage + aes(colour = species)
MonNuageCouleur

Comme dit et re-dit précédemment, à chaque aesthetics est associée une échelle. On a ajouté l’aesthetics colour, donc il y a une échelle associée.

Pour la modifier et utiliser par exemple des palettes (qualitatives dans le cas présent) de couleur du package RcolorBrewer

library("RColorBrewer")
display.brewer.all()

utilisons la fonction scale_colour_brewer()

MonNuageCouleur + scale_colour_brewer(palette = "Set2")

MonNuageCouleur + scale_colour_brewer(palette = "Set3")

ou d’autres fonctions

MonNuageCouleur + scale_colour_grey()

MonNuageCouleur + scale_colour_hue(l = 50, c = 30)

Et pour mettre ses couleurs favorites, on peut même le faire à la main.

MonNuageCouleur +
  scale_colour_manual(values = c("rosybrown", "tomato", "midnightblue"))

En cas de manque d’inspiration, la fonction colours() est là.

Pour des couleurs liées à une variable quantitative

Et pour une légende en couleur liée à une variable quantitative, comment ça marche ? Pareil !

Modifions l’aesthetic colour

MonNuageCouleurQuanti <- MonNuage + aes(colour = bill_depth_mm)
MonNuageCouleurQuanti

On a, comme on l’a déjà vu, une teinte de bleu qui s’éclaircit quand la valeur de bill_depth_mm augmente. Et si je veux changer du bleu ?

On peut encore aller piocher dans les palettes (séquentielles cette fois-ci) du package RColorBrewer, mais, là dans ggplot2, on ne brasse plus, on distille.

MonNuageCouleurQuanti + scale_colour_distiller(palette = "Greens")

MonNuageCouleurQuanti + scale_colour_distiller(palette = "Purples")

Pas mal, mais je ne vois pas bien les points les plus clairs. Pas de problème, je vais passer en theme_dark.

MonNuageCouleurQuanti +
  scale_colour_distiller(palette = "Purples") +
  theme_dark()

On peut aussi récupérer d’autres palettes de R

MonNuageCouleurQuanti + scale_colour_gradientn(colors = terrain.colors(5))

et faire en fait tout ce que l’on veut ! (mais ça, on le savait déjà).

MonNuageCouleurQuanti + scale_colour_gradient(low = "red", high = "blue")

MonNuageCouleurQuanti +
  scale_colour_gradient(low = "red", high = "blue", breaks = seq(12, 20, by = 3))

Pour des formes aussi !

A un aesthetic correspond une échelle, donc si mon aesthetic est une forme, alors une échelle lui est associée (pas de discrimination !) et on peut donc la modifier avec les fonctions scale_shape_*().

MonNuageForme <- MonNuage + aes(shape = species)
MonNuageForme + scale_shape(solid = FALSE)

MonNuageForme + scale_shape_manual(values = c(4, 8, 15))

MonNuageForme + scale_shape_manual(values = c("*", "@", "x"))

Et si je veux modifier quelques bricoles dans ma légende, aucun problème, je peux aussi faire ce genre de choses :

MonNuageForme + scale_shape(name = "Espèces",
                            labels = c("Ad.","Ch.","Ge."))

Pour aller plus loin dans les modifications d’apparence de la légende (et d’autres choses, en fait de tout ce que l’on voit), il faut aller bricoler le thème… Ca devient très touchy et pas forcément d’une grande utilité, mais voyons quand même 2 ou 3 trucs. Un petit coup d’oeil à l’aide de la fonction theme() peut suffire à se convaincre de ne pas aller plus loin.

MonNuageCouleur + ggtitle("Mon joli graphique") +
  theme(legend.position = "bottom",
        legend.title = element_text(face = "bold"),
        axis.title.x = element_text(face = "italic"),
        plot.title = element_text(colour = "purple", 
                                  size = rel(1.5),
                                  hjust = 0.5))       # centrage du titre

Mon graphique est parfait, je veux le sauvegarder !

Deux solutions possibles :

  • Je sauve l’objet dans mon environnement de travail : fonction save(), en fait comme n’importe quel objet R. En fait, ça, on l’a déjà pratiqué.

  • Je sauvegarde la figure dans un fichier graphique : fonction ggsave(). Consulter l’aide de ggsave() pour en savoir plus, mais ne vous attendez pas à de grandes surprises. Seule précaution à signaler : il faut réaliser le graphique, puis le sauvegarder (pas comme avec les fonctions pdf(), jpeg(), png() qu’il faut appeler avant de réaliser le graphique et qu’il faut ensuite fermer avec dev.off()).

ggsave("perfect!.png")

Quelques extensions sympas

A best-of:

  • {patchwork}: Combine/Assemble ggplots.
  • {ggforce}: Provide missing functionality to ggplot2.
  • {ggtext}: Improved text rendering support for ggplot2.

Interactive packages

  • {plotly}: makes interactive, publication-quality graphs (with ggplot2 or not).
  • {ggedit}: Interactively edit ggplot layers, scales and themes aesthetics.
  • {esquisse}: Interactively explore your data by visualizing it with the ggplot2 package.

Other useful packages

Quelques extensions sympas

Disposition de plusieurs graphiques

patchwork

library(patchwork)

Le package patchwork permet de disposer plusieurs graphiques de façon très simple. Démonstration avec des + et des `/’ !

MonNuage + MonNuageCouleur

MonNuage + MonNuageCouleur + MonNuageForme

MonNuage / MonNuageCouleur

MonNuage / MonNuageCouleur / MonNuageForme

MonNuage + (MonNuageCouleur / MonNuageForme)

MonNuage / (MonNuageCouleur + MonNuageForme)

Avec un peu de | aussi !

MonNuage + MonNuageCouleur + MonNuageForme + MonNuageTest

MonNuage | MonNuageCouleur | MonNuageForme | MonNuageTest

MonNuage + MonNuageCouleur + MonNuageForme + MonNuageTest + plot_layout(ncol = 3)

MonNuage + MonNuageCouleur + MonNuageForme + MonNuageTest + plot_layout(width = c(2,1))

Et ce qui est bien aussi, c’est de pouvoir étiqueter chaque graphique…

patchwork <- (MonNuage + MonNuageCouleur) / MonNuageForme
patchwork + 
  plot_layout(guides = "collect") +
  plot_annotation(tag_levels = 'A',title="un super graphique")  & theme(legend.position = "bottom")

…et de les aérer un peu.

MonNuage + plot_spacer() + MonNuageCouleur + plot_spacer() + MonNuageForme

ou au contraire de les superposer

MonNuageCouleur + inset_element(MonNuage, left = 0.6, bottom = 0.5, right = 1, top  = 0.85)

Pour en apprendre plus : https://patchwork.data-imaginist.com/articles/patchwork.html

cowplot

library(cowplot)
  • Pour changer de thème
MonNuageCouleur + theme_cowplot()

MonNuageCouleur + theme_minimal_grid()

MonNuageCouleur + theme_minimal_hgrid()

  • Pour disposer plusieurs graphiques (comme patchwork)
plot_grid(MonNuageCouleurQuanti, MonNuageCouleur, labels = c('1', '2'))

plot_grid(MonNuageCouleurQuanti, MonNuageCouleur, labels = "AUTO")

plot_grid(MonNuageCouleurQuanti, MonNuageCouleur, labels = "auto")

Jusqu’ici tout va bien, mais là, ça coince un peu non ?

MonNuageCouleurAxe <- MonNuageCouleur + theme(axis.text.x = element_text(size = rel(3), angle = 90),
                                              axis.text.y = element_text(size = rel(3)))
plot_grid(MonNuageCouleurAxe, MonNuageCouleur)

Non ? pas de souci ? et là, n’est-ce pas mieux ?

plot_grid(MonNuageCouleurAxe, MonNuageCouleur, align = "h")

Et oui, c’est quand même mieux d’aligner les 2 graphiques !

Et dans l’autre sens :

plot_grid(MonNuageCouleurAxe, MonNuageCouleur, ncol = 1)

plot_grid(MonNuageCouleurAxe, MonNuageCouleur, ncol = 1, align = "v")

Pour en savoir plus : https://cran.r-project.org/web/packages/cowplot/vignettes/introduction.html

ggforce

Une librairie très complète qui ajoute beaucoup de fonctionnalités manquantes à ggplot2.

Voir ici : https://ggforce.data-imaginist.com/reference/index.html.

  • facet_zoom(): Create a zoom facet for a plot.
  • facet_matrix(): Allow to put differents columns into different rows and columns in a grid of panels.
  • geom_mark_*: Graphical annotation of data.

ggforce

require(ggforce)
ggplot(penguins, aes(bill_length_mm, flipper_length_mm, colour = species)) +
    geom_point() +
    facet_zoom(xy = species == "Adelie")

ggforce::facet_zoom()

ggplot(penguins, aes(bill_length_mm, flipper_length_mm, colour = species)) +
    geom_point() +
  facet_zoom(xlim = c(40, 50))

ggforce::facet_zoom() for my zooming issue

p_boxplot <- ggplot(penguins, aes(x = species, y = bill_length_mm)) +
  geom_boxplot() +
  geom_point(aes(size = bill_depth_mm),
             alpha = 0.2, position = "jitter")+ theme(legend.position ="bottom")

p_boxplot  +  facet_zoom(ylim = c(40,50))

ggforce::facet_matrix()

ggplot(penguins %>% tidyr::drop_na(), aes(col = sex, fill = sex)) +
  geom_autopoint() +
  geom_autodensity() +
  facet_matrix(rows = vars(island:body_mass_g), layer.diag = 2)

ggforce::facet_matrix()

penguins %>% 
  ggplot(aes(x=species,y=bill_length_mm,fill=species)) +
  geom_boxplot() +
  ggforce::facet_col(~island)

ggforce::facet_matrix()

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
7            4.6         3.4          1.4         0.3     setosa
8            5.0         3.4          1.5         0.2     setosa
9            4.4         2.9          1.4         0.2     setosa
10           4.9         3.1          1.5         0.1     setosa
11           5.4         3.7          1.5         0.2     setosa
12           4.8         3.4          1.6         0.2     setosa
13           4.8         3.0          1.4         0.1     setosa
14           4.3         3.0          1.1         0.1     setosa
15           5.8         4.0          1.2         0.2     setosa
16           5.7         4.4          1.5         0.4     setosa
17           5.4         3.9          1.3         0.4     setosa
18           5.1         3.5          1.4         0.3     setosa
19           5.7         3.8          1.7         0.3     setosa
20           5.1         3.8          1.5         0.3     setosa
21           5.4         3.4          1.7         0.2     setosa
22           5.1         3.7          1.5         0.4     setosa
23           4.6         3.6          1.0         0.2     setosa
24           5.1         3.3          1.7         0.5     setosa
25           4.8         3.4          1.9         0.2     setosa
26           5.0         3.0          1.6         0.2     setosa
27           5.0         3.4          1.6         0.4     setosa
28           5.2         3.5          1.5         0.2     setosa
29           5.2         3.4          1.4         0.2     setosa
30           4.7         3.2          1.6         0.2     setosa
31           4.8         3.1          1.6         0.2     setosa
32           5.4         3.4          1.5         0.4     setosa
33           5.2         4.1          1.5         0.1     setosa
34           5.5         4.2          1.4         0.2     setosa
35           4.9         3.1          1.5         0.2     setosa
36           5.0         3.2          1.2         0.2     setosa
37           5.5         3.5          1.3         0.2     setosa
38           4.9         3.6          1.4         0.1     setosa
39           4.4         3.0          1.3         0.2     setosa
40           5.1         3.4          1.5         0.2     setosa
41           5.0         3.5          1.3         0.3     setosa
42           4.5         2.3          1.3         0.3     setosa
43           4.4         3.2          1.3         0.2     setosa
44           5.0         3.5          1.6         0.6     setosa
45           5.1         3.8          1.9         0.4     setosa
46           4.8         3.0          1.4         0.3     setosa
47           5.1         3.8          1.6         0.2     setosa
48           4.6         3.2          1.4         0.2     setosa
49           5.3         3.7          1.5         0.2     setosa
50           5.0         3.3          1.4         0.2     setosa
51           7.0         3.2          4.7         1.4 versicolor
52           6.4         3.2          4.5         1.5 versicolor
53           6.9         3.1          4.9         1.5 versicolor
54           5.5         2.3          4.0         1.3 versicolor
55           6.5         2.8          4.6         1.5 versicolor
56           5.7         2.8          4.5         1.3 versicolor
57           6.3         3.3          4.7         1.6 versicolor
58           4.9         2.4          3.3         1.0 versicolor
59           6.6         2.9          4.6         1.3 versicolor
60           5.2         2.7          3.9         1.4 versicolor
61           5.0         2.0          3.5         1.0 versicolor
62           5.9         3.0          4.2         1.5 versicolor
63           6.0         2.2          4.0         1.0 versicolor
64           6.1         2.9          4.7         1.4 versicolor
65           5.6         2.9          3.6         1.3 versicolor
66           6.7         3.1          4.4         1.4 versicolor
67           5.6         3.0          4.5         1.5 versicolor
68           5.8         2.7          4.1         1.0 versicolor
69           6.2         2.2          4.5         1.5 versicolor
70           5.6         2.5          3.9         1.1 versicolor
71           5.9         3.2          4.8         1.8 versicolor
72           6.1         2.8          4.0         1.3 versicolor
73           6.3         2.5          4.9         1.5 versicolor
74           6.1         2.8          4.7         1.2 versicolor
75           6.4         2.9          4.3         1.3 versicolor
76           6.6         3.0          4.4         1.4 versicolor
77           6.8         2.8          4.8         1.4 versicolor
78           6.7         3.0          5.0         1.7 versicolor
79           6.0         2.9          4.5         1.5 versicolor
80           5.7         2.6          3.5         1.0 versicolor
81           5.5         2.4          3.8         1.1 versicolor
82           5.5         2.4          3.7         1.0 versicolor
83           5.8         2.7          3.9         1.2 versicolor
84           6.0         2.7          5.1         1.6 versicolor
85           5.4         3.0          4.5         1.5 versicolor
86           6.0         3.4          4.5         1.6 versicolor
87           6.7         3.1          4.7         1.5 versicolor
88           6.3         2.3          4.4         1.3 versicolor
89           5.6         3.0          4.1         1.3 versicolor
90           5.5         2.5          4.0         1.3 versicolor
91           5.5         2.6          4.4         1.2 versicolor
92           6.1         3.0          4.6         1.4 versicolor
93           5.8         2.6          4.0         1.2 versicolor
94           5.0         2.3          3.3         1.0 versicolor
95           5.6         2.7          4.2         1.3 versicolor
96           5.7         3.0          4.2         1.2 versicolor
97           5.7         2.9          4.2         1.3 versicolor
98           6.2         2.9          4.3         1.3 versicolor
99           5.1         2.5          3.0         1.1 versicolor
100          5.7         2.8          4.1         1.3 versicolor
101          6.3         3.3          6.0         2.5  virginica
102          5.8         2.7          5.1         1.9  virginica
103          7.1         3.0          5.9         2.1  virginica
104          6.3         2.9          5.6         1.8  virginica
105          6.5         3.0          5.8         2.2  virginica
106          7.6         3.0          6.6         2.1  virginica
107          4.9         2.5          4.5         1.7  virginica
108          7.3         2.9          6.3         1.8  virginica
109          6.7         2.5          5.8         1.8  virginica
110          7.2         3.6          6.1         2.5  virginica
111          6.5         3.2          5.1         2.0  virginica
112          6.4         2.7          5.3         1.9  virginica
113          6.8         3.0          5.5         2.1  virginica
114          5.7         2.5          5.0         2.0  virginica
115          5.8         2.8          5.1         2.4  virginica
116          6.4         3.2          5.3         2.3  virginica
117          6.5         3.0          5.5         1.8  virginica
118          7.7         3.8          6.7         2.2  virginica
119          7.7         2.6          6.9         2.3  virginica
120          6.0         2.2          5.0         1.5  virginica
121          6.9         3.2          5.7         2.3  virginica
122          5.6         2.8          4.9         2.0  virginica
123          7.7         2.8          6.7         2.0  virginica
124          6.3         2.7          4.9         1.8  virginica
125          6.7         3.3          5.7         2.1  virginica
126          7.2         3.2          6.0         1.8  virginica
127          6.2         2.8          4.8         1.8  virginica
128          6.1         3.0          4.9         1.8  virginica
129          6.4         2.8          5.6         2.1  virginica
130          7.2         3.0          5.8         1.6  virginica
131          7.4         2.8          6.1         1.9  virginica
132          7.9         3.8          6.4         2.0  virginica
133          6.4         2.8          5.6         2.2  virginica
134          6.3         2.8          5.1         1.5  virginica
135          6.1         2.6          5.6         1.4  virginica
136          7.7         3.0          6.1         2.3  virginica
137          6.3         3.4          5.6         2.4  virginica
138          6.4         3.1          5.5         1.8  virginica
139          6.0         3.0          4.8         1.8  virginica
140          6.9         3.1          5.4         2.1  virginica
141          6.7         3.1          5.6         2.4  virginica
142          6.9         3.1          5.1         2.3  virginica
143          5.8         2.7          5.1         1.9  virginica
144          6.8         3.2          5.9         2.3  virginica
145          6.7         3.3          5.7         2.5  virginica
146          6.7         3.0          5.2         2.3  virginica
147          6.3         2.5          5.0         1.9  virginica
148          6.5         3.0          5.2         2.0  virginica
149          6.2         3.4          5.4         2.3  virginica
150          5.9         3.0          5.1         1.8  virginica
iris %>% ggplot() +
  geom_boxplot(aes(x = .panel_x, y = .panel_y, fill=Species)) +
  facet_matrix(rows = vars(-Species),cols = vars(Species))

ggforce::geom_mark_*()

ggplot(penguins, aes(x = flipper_length_mm, y = bill_length_mm)) +
  geom_mark_hull(aes(fill = species, label = species)) +
  geom_point()

ggforce::geom_mark_*()

ggplot(penguins %>% drop_na(), aes(bill_depth_mm, body_mass_g)) +
  geom_point() +
  geom_mark_rect(aes(filter = species == "Gentoo", 
                        label = "Gentoo", 
                        description = "Gentoo penguins seem quite different from the other species"),
                        label.fill = "pink",
                        label.colour = "red", 
                        con.colour = "red",
                        con.linetype = 2, 
                        expand = unit(0.01, "mm"), 
                        fill = "pink") 

From: https://ihaddadenfodil.com/post/it-s-a-bird-it-s-a-plane-it-s-a-ggforce-function/

ggtext: Improved text rendering support for ggplot2 by adding Markdown and html syntax.

Origine du code et des images: Laura Navarro

require(ggtext)
labels <- c(
  Adelie = "<img src='imgs/adelie.png'
    width='80' /><br>Adelie",
  Gentoo = "<img src='imgs/gentoo_1.png'
    width='80' /><br>Gentoo",
  Chinstrap = "<img src='imgs/chinstrap.png'
    width='80' /><br>Chinstrap"
)

pggtext <- penguins %>% drop_na() %>% ggplot(aes(x=species,y = bill_length_mm, col = sex)) +
  geom_boxplot() +
  scale_color_manual(values = c("#66b3ff", "#8c8c8c"))+
  ylab("length (mm)") +
  xlab("Species") +
  theme_minimal() +
  theme(legend.position = "bottom",
        legend.text = element_text(size = 11),
        legend.title = element_blank(),
        panel.grid.minor = element_blank(),
        axis.title = element_text(color = "white", size = 10),
        plot.title = element_text(size = 20),
        plot.subtitle = element_text( size = 12, hjust = 1)
  )

pggtext +
  scale_x_discrete(
    name = NULL,
    labels = labels
  ) +
  theme(
    axis.text.x = element_markdown()
  )

ggtext: Bold, italic and color to ggplot2

df_penguins <- penguins %>% 
  select(species, bill_length_mm:flipper_length_mm,sex) %>% 
  pivot_longer(bill_length_mm:flipper_length_mm) %>% 
  drop_na(sex) %>% 
  mutate(
    name = name %>% 
      str_replace_all("_", " ") %>% 
      str_remove_all(pattern = " mm") %>% 
      str_to_title()
  ) %>%
  mutate(name = glue::glue("**{name}** (*mm*)")) %>% 
  mutate(my_color = case_when( 
    sex == "male" ~ "#66b3ff", 
    sex == "female" ~ "#8c8c8c"
  )) %>%  
  mutate(sex = glue::glue("<span style = 'color:{my_color};'>{sex}</span>"))

df_penguins
# A tibble: 999 × 5
   species sex                                          name      value my_color
   <fct>   <glue>                                       <glue>    <dbl> <chr>   
 1 Adelie  <span style = 'color:#66b3ff;'>male</span>   **Bill L…  39.1 #66b3ff 
 2 Adelie  <span style = 'color:#66b3ff;'>male</span>   **Bill D…  18.7 #66b3ff 
 3 Adelie  <span style = 'color:#66b3ff;'>male</span>   **Flippe… 181   #66b3ff 
 4 Adelie  <span style = 'color:#8c8c8c;'>female</span> **Bill L…  39.5 #8c8c8c 
 5 Adelie  <span style = 'color:#8c8c8c;'>female</span> **Bill D…  17.4 #8c8c8c 
 6 Adelie  <span style = 'color:#8c8c8c;'>female</span> **Flippe… 186   #8c8c8c 
 7 Adelie  <span style = 'color:#8c8c8c;'>female</span> **Bill L…  40.3 #8c8c8c 
 8 Adelie  <span style = 'color:#8c8c8c;'>female</span> **Bill D…  18   #8c8c8c 
 9 Adelie  <span style = 'color:#8c8c8c;'>female</span> **Flippe… 195   #8c8c8c 
10 Adelie  <span style = 'color:#8c8c8c;'>female</span> **Bill L…  36.7 #8c8c8c 
# ℹ 989 more rows
df_penguins %>% ggplot(aes(x=name,y=value,col = sex,fill=after_scale(colorspace::lighten(color, .5)))) +
  geom_bar(position="dodge",stat = "summary", fun = "mean") +
  facet_wrap(~species,labeller = labeller(species = labels)) +
  scale_color_manual(values = c("#66b3ff", "#8c8c8c"))+
  theme_minimal() +theme(
    axis.text.y   = element_markdown(),
    legend.text = element_markdown(),
    strip.text = element_markdown(),
    legend.position = "bottom"
  ) +coord_flip()

Interactivité

library(plotly)

L’utilisation de ggplot2 avec plotly n’est pas obligatoire, mais maintenant que l’on connaît, on ne va quand même pas s’en priver.

Alors allons-y !

ggplotly(MonNuageCouleur)

Il n’y a plus qu’à promener sa souris sur le graphique pour voir ce que cela donne.

En explorant un peu l’aide en ligne, on peut gérer ce qui apparaît sous le curseur de la souris quand on le déplace.

ggplotly(MonNuageCouleur, tooltip = c("x","y"))
ggplotly(MonNuageCouleur, tooltip = c("colour"))

Et on peut bien sûr l’utiliser sur des graphiques autres que nuages de points.

MesBoxplots <- ggplot(penguins,aes(x=species, y=body_mass_g,
                                colour=species)) + geom_boxplot()
ggplotly(MesBoxplots)
ggplotly(Histogramme)

Noter que l’interactivité reste disponible dans un fichier html.

Publication ready plots

The ggpubr package provides some easy-to-use functions for creating and customizing ggplot2- based publication ready plots.

C’est un peu dommage de se priver de la syntaxe de ggplot2 une fois qu’on la connaît, mais il y a peut-être des choses sympas à trouver dans ce package.

library(ggpubr)
ggboxplot(penguins, x = "species", y  = "body_mass_g")

MonGraphiqueggpubr <- ggboxplot(penguins, x = "species", y  = "body_mass_g",
                                color = "species", add = "jitter")
MonGraphiqueggpubr

Pour ajouter la p-value d’un test de Kruskal-Wallis (par défaut), c’est immédiat.

MonGraphiqueggpubr + stat_compare_means()

et si on préfère l’ANOVA, pas de souci

MonGraphiqueggpubr + stat_compare_means(method = "anova")

Jusqu’ici, pas de quoi sauter au plafond, on aurait gérer plus ou moins bien le truc avec un peu plus de code. En revanche, pour les comparaisons 2 à 2 , c’est quand même pas mal.

MesComparaisons <- list( c("Adelie", "Chinstrap"),
                         c("Adelie", "Gentoo"),
                         c("Gentoo", "Chinstrap") )
MonGraphiqueggpubr + stat_compare_means(comparisons = MesComparaisons)

Pour la totale :

MonGraphiqueggpubr +
  stat_compare_means(comparisons = MesComparaisons) +
  stat_compare_means(label.y = 5)

avec des étoiles selon le découpage indiqué dans l’aide de la fonction stat_compare_means :

symnum.args <- list(cutpoints = c(0, 0.0001, 0.001, 0.01, 0.05, 1), symbols = c("****", "***", "**", "*", "ns"))
MonGraphiqueggpubr +
  stat_compare_means(comparisons = MesComparaisons, label = "p.signif") +
  stat_compare_means(label.y = 5)

Et si je préfère les violons (avec les boxplots à l’intérieur !)

ggviolin(penguins, x = "species", y  = "body_mass_g",
                                color = "species", add = "boxplot") +
  stat_compare_means(comparisons = MesComparaisons, label = "p.signif") +
  stat_compare_means(label.y = 5)

Et d’autres choses plus ou moins utiles à voir ici : https://rpkgs.datanovia.com/ggpubr

Ressources

Voici quelques liens pour étoffer le survol que nous venons de faire.

Et maintenant, amusez-vous !

⚠️ **GitHub.com Fallback** ⚠️