Charly Marie
  • Home
  • Research
  • Behavioural science
  • Academic teaching
  • Seminars
  • CV
  • Blog

Sur cette page

  • Point de départ
  • Extraire les moyennes marginales et les envoyer vers un graphique ggplot2
    • Extraire les moyennes marginales du modèle
    • Représenter les moyennes marginales du modèle
  • Références

Transformer une sortie R (moche) en un graphique (beau)

  • Montrer tout le code
  • Cacher tout le code

  • Voir les sources
R
Modèle linéaire
Fonction
Suite à mon dernier billet (ici), on m’a fait remarquer qu’il pouvait être utile de transformer une sortie de régression type lm() en graphique. Je propose deux façons de faire, en ggplot2.
Date de publication

23 avril 2026


Point de départ

Dans mon dernier billet de blog (ici, qui date un peu, je sais), je proposais des fonctions pour automatiser l’extraction des coefficients d’un modèle de type lm() en R. Mais voilà, on peut potentiellement préférer un (joli) graphique, plutôt qu’une (jolie) table. Aussi jolie soit-elle. Voici donc une manière rapide, facile et personnalisable pour transformer des moyennes marginales en graphique, en R.

Je repars à nouveau du jeu de données penguins (Horst et al., 2020), pour lequel j’ajuste le modèle suivant :

\[ \begin{aligned} \text{masse}_i = \; & \alpha + \beta_1 \, \mathbf{1}(\text{espèce}_i = \text{Jugulaire}) + \beta_2 \, \mathbf{1}(\text{espèce}_i = \text{Papou}) \\ & + \beta_3 \, \mathbf{1}(\text{sexe}_i = \text{Mâle}) + \beta_4 \, \mathbf{1}(\text{espèce}_i = \text{Jugulaire}) \times \mathbf{1}(\text{sexe}_i = \text{Mâle}) \\ & + \beta_5 \, \mathbf{1}(\text{espèce}_i = \text{Papou}) \times \mathbf{1}(\text{sexe}_i = \text{Mâle}) + \varepsilon_i \end{aligned} \tag{1}\]

Chaque \(\mathbf{1}(\cdots)\) est un indicateur qui prend 1 quand l’individu \(i\) correspond à la catégorie, et 0 autrement. La catégorie de référence, lorsque tous les \(\mathbf{1}(\cdots)\) valent 0, correspond aux femelles Adélie et \(\alpha\) est leur masse moyenne.

Dit autrement, je régresse la masse sur l’espèce, le sexe, et l’interaction espèce x sexe. C’est une légère différence par rapport au dernier post, qui n’incluait pas d’interaction. La sortie est la suivante :

Paramètre Estimation
[IC 95 %]
Erreur standard Statistique t (327) p-valeur β
[IC 95 %]
(Intercept) 3368.84
[3297.6; 3440.07]
36.21 93.03 < .001 -1.04
[-1.13; -0.95]
Espèce : Jugulaire 158.37
[31.99; 284.75]
64.24 2.47 0.014 0.2
[0.04; 0.35]
Espèce : Papou 1310.91
[1203.84; 1417.97]
54.42 24.09 < .001 1.63
[1.5; 1.76]
Sexe : Mâle 674.66
[573.91; 775.4]
51.21 13.17 < .001 0.84
[0.71; 0.96]
Espèce : Jugulaire x Sexe : Mâle -262.89
[-441.62; -84.17]
90.85 -2.89 0.004 -0.33
[-0.55; -0.1]
Espèce : Papou x Sexe : Mâle 130.44
[-19.93; 280.8]
76.44 1.71 0.089 0.16
[-0.02; 0.35]
Note : n = 333 observations.
Table 1: Régression de la masse des manchots en grammes sur l’espèce, le sexe, et l’interaction espèce x sexe, selon l’Équation 1. Sortie de la fonction lm_summary() passée via lm_tab().

Cette table renvoie déjà toutes les informations nécessaires pour interpréter le modèle (si, si : on en parlera une prochaine fois). Mais, on pourrait malgré tout lui préférer une représentation graphique directement estimée à partir du modèle.

Extraire les moyennes marginales et les envoyer vers un graphique ggplot2

Cela nécessite deux étapes :

  1. Extraire les moyennes marginales du modèle.
  2. Représenter ces moyennes marginales.

Extraire les moyennes marginales du modèle

(Presque) rien de plus simple, via le package marginaleffects (Arel-Bundock et al., 2024) et sa fonction avg_predictions() :

Voir le code
lm_1_mean <- avg_predictions(lm_1, variables = c("espece", "sexe"), df = insight::get_df(lm_1))

lm_1_mean %>%
  knitr::kable()
espece sexe estimate std.error statistic p.value s.value conf.low conf.high df
Adélie Femelle 3368.836 36.21221 93.03037 0 785.9444 3297.597 3440.074 327
Adélie Mâle 4043.493 36.21216 111.66118 0 869.4277 3972.255 4114.731 327
Jugulaire Femelle 3527.206 53.06154 66.47387 0 635.4447 3422.821 3631.591 327
Jugulaire Mâle 3938.971 53.06147 74.23410 0 684.3013 3834.586 4043.356 327
Papou Femelle 4679.741 40.62587 115.19117 0 883.7478 4599.820 4759.662 327
Papou Mâle 5484.836 39.61435 138.45578 0 968.7845 5406.905 5562.767 327
Table 2: Régression de la masse des manchots en grammes, sur l’espèce et le sexe, selon l’Équation 1. Sortie de la fonction avg_predictions() passée via knitr::kable().

Cette table ne fournit aucune information supplémentaire comparée à la table de régression1, mais va nous permettre de réaliser notre graphique des moyennes marginales.

Représenter les moyennes marginales du modèle

Ici, il nous suffit de repartir de l’objet qui stocke les moyennes marginales, pour l’envoyer vers ggplot() (Wickham, 2016). Cet objet contient, parmis d’autres, les deux informations qui nous intéressent ici :

  • La masse moyenne, par espèce, en fonction du sexe.
  • L’intervalle de confiance à 95 % de cette masse moyenne.

Je vous propose deux solutions, selon vos préférences. Aucun n’est vraiment mieux que l’autre :

  • Le premier, à gauche, met l’accent sur la comparaison entre espèces, à sexe constant.
  • Le second, à droite, met l’accent sur la comparaison entre sexes, à espèce constante.
Voir le code
# Un premier graphique, qui représente les  données en deux sous-facettes selon le sexe
p1 <- lm_1_mean %>%
  
  ggplot() +
  aes(x = espece, y = estimate, fill = sexe) +
  geom_col() +
  geom_errorbar(aes(ymin = conf.low, ymax = conf.high), width = 0) +
  scale_fill_manual(values = c(Femelle = egypt[1], Mâle = egypt[2])) +
  facet_wrap(~ sexe) + # C'est ici que ça se passe, pour représenter les données en deux sous-facettes
  labs(
    y = "Moyennes marginales de la masse (g)",
    fill = "Sexe",
    title = "Masse des manchots, selon l’espèce et le sexe") +
  theme(axis.text.x = element_text())

# Un second graphique, qui représente les données côte à côte
p2 <- lm_1_mean %>%
  ggplot() +
  aes(x = espece, y = estimate, fill = sexe) +
  geom_col(position = position_dodge(width = 0.8), width = 0.8) + # C'est ici que ça se passe, via position_dodge(), pour représenter les données côte à côte
  geom_errorbar(aes(ymin = conf.low, ymax = conf.high),
    position = position_dodge(width = 0.8),
    width = 0.0) +
  scale_fill_manual(values = c(Femelle = egypt[1], `Mâle` = egypt[2])) +
  labs(
    y = "Moyennes marginales de la masse (g)",
    fill = "Sexe",
    title = "Masse des manchots, selon l’espèce et le sexe") +
  theme(axis.text.x = element_text())

ggarrange(
  p1, p2,
  common.legend = T,
  legend = "bottom")
Figure 1: Masse des manchots en grammes, selon l’espèce et le sexe. La barre représente l’intervalle de confiance, à 95 %.

Cette Figure 1 ne dit rien de plus ou de moins que la Table 1, mais le fait différemment, en étant certainement plus simple à lire qu’une table de régression.

Références

Arel-Bundock, V., Greifer, N., & Heiss, A. (2024). How to Interpret Statistical Models Using marginaleffects for R and Python. Journal of Statistical Software, 111(9), 1‑32. https://doi.org/10.18637/jss.v111.i09
Horst, A. M., Hill, A. P., & Gorman, K. B. (2020). palmerpenguins: Palmer Archipelago (Antarctica) penguin data. https://doi.org/10.5281/zenodo.3960218
Wickham, H. (2016). ggplot2: Elegant Graphics for Data Analysis. Springer-Verlag New York. https://ggplot2.tidyverse.org

Notes de bas de page

  1. L’intercept renvoie déjà la masse moyenne, pour un manchot de l’espèce de référence Adélie, de sexe de référence Femelle; le coefficient de Espèce : Jugulaire renvoie déjà la différence moyenne, pour un manchot de l’espèce Jugulaire, de sexe femelle, par rapport à l’intercept; etc…).↩︎

Code source
---
title: "Transformer une sortie `R` (moche) en un graphique (beau)"
date: 04-23-2026
categories: ["R", "Modèle linéaire", "Fonction"]
tags: ["Technique", "R"]
description: "Suite à mon dernier billet ([ici](https://charlymarie.github.io/posts/03%20-%20lm_summary()/lm_summary().html){target='_blank'}), on m'a fait remarquer qu'il pouvait être utile de transformer une sortie de régression type `lm()` en graphique. Je propose deux façons de faire, en `ggplot2`."
lang: fr
body-classes: indent-paragraphs
format:
  html:
    code-fold: true
    code-summary: "Voir le code"
    code-overflow: wrap
    code-tools: true
    code-block-bg: true
execute:
  warning: false
  error: false
draft: false
---

<hr style="border:1px solid blue" />

```{r echo=FALSE}
library(tidyverse); library(ggtext); library(marginaleffects); library(ggpubr)

# Définir les fonctions pour extraire les paramètres d'un lm()
lm_summary <- function(model, vcov = NULL) {
  
  # Extraire les paramètres non standardisés
  mp <- parameters::model_parameters(model,
                                     vcov = vcov)
  
  # Extraire les paramètres standardisés (refit le modèle)
  mp_std <- dplyr::select(
    parameters::model_parameters(
      model,
      standardize = "refit",
      vcov = vcov),
    
    Parameter,
    Std_Coefficient = Coefficient,
    Std_CI_low = CI_low,
    Std_CI_high = CI_high)
  
  mp <- dplyr::left_join(mp, mp_std, by = "Parameter")
  
  # Préparer les données
  mp <- dplyr::mutate(mp,
                      
                      # Arrondir p à trois décimales
                      p = ifelse(p < .001, "< .001", round(p, 3)),
                      
                      # Arrondir les autres valeurs à deux décimales
                      dplyr::across(dplyr::where(is.numeric), ~ round(.x, 2)),
                      
                      # Réunir l'intervalle de confiance de l'estimate et le placer sous le Coefficient
                      Coefficient = paste0(Coefficient, "<br>", "[", CI_low, "; ", CI_high, "]"),
                      
                      # Réunir l'intervalle de confiance de β et le placer sous β
                      β = paste0(Std_Coefficient, "<br>", "[", Std_CI_low, "; ", Std_CI_high, "]"),
                      
                      # Conserver le modèle pour lm_tab, même après des verbes dplyr (mutate, filter, etc.)
                      .model = list(model))
  
  # Renommer les colonnes : les colonnes avec un nom simple
  mp <- dplyr::rename(mp,
                      Paramètre = Parameter,
                      Estimation = Coefficient,
                      "Erreur standard" = SE,
                      p = p)
  
  # Renommer les colonnes : la colonne t est un peu plus complexe, car je souhaite y intégrer le nombre de degrés de liberté de la statistique
  mp <- dplyr::rename_with(mp,
                           ~ paste0("Statistique t (", broom::glance(model)$df.residual, ")"),
                           .cols = t)
  
  # Ne pas garder les colonnes qui ont déjà été incorporées à d'autres, ou ne sont pas utiles
  mp <- dplyr::select(mp,
                      -c(CI, CI_low, CI_high, df_error, Std_Coefficient, Std_CI_low, Std_CI_high))
  
  mp
}

lm_tab <- function(lm_summary) {
  
  lm_model <- attr(lm_summary, "model")
  if (is.null(lm_model) && ".model" %in% names(lm_summary)) {
    lm_model <- lm_summary$.model[[1]]
  }
  if (is.null(lm_model)) {
    stop("Aucun modèle retrouvé en sortie de lm_summary.")
  }
  
  g <- broom::glance(lm_model)
  t_col <- paste0("Statistique t (", g$df.residual, ")")
  
  tab <- gt::gt(lm_summary)
  
  # Ne pas afficher la colonne technique, qui contient le modèle
  tab <- gt::cols_hide(tab, columns = dplyr::any_of(".model"))
  
  tab <- gt::cols_label(
    tab,
    Paramètre = gt::html("<strong>Paramètre</strong>"),
    Estimation = gt::html("<strong>Estimation<br>[IC 95 %]</strong>"),
    `Erreur standard` = gt::html("<strong>Erreur standard</strong>"),
    !!t_col := gt::html(
      paste0("<strong>Statistique <em>t</em> (", g$df.residual, ")</strong>")),
    p = gt::html("<strong><em>p</em>-valeur</strong>"),
    β = gt::html("<strong><em>β</em><br>[IC 95 %]</strong>"))
  
  tab <- gt::fmt_markdown(tab, columns = c(Estimation, β))
  
  tab <- gt::cols_align(
    tab,
    align = "center",
    columns = c(Estimation, `Erreur standard`, dplyr::all_of(t_col), p, β))
  
  tab <- gt::tab_source_note(
    tab,
    gt::html(paste0("<em>Note</em> : n = ",
                    scales::number(g$nobs),
                    " observations.")))
  
  tab
}

#### Construire un thème ggplot() personnalisé ####
# Quasi entièrement copié d'Andrew Heiss: https://www.andrewheiss.com/blog/2021/12/20/fully-bayesian-ate-iptw/
# Avec l'aide de Nicolas Rennie: https://nrennie.rbind.io/art-of-viz/programming-languages.html

egypt <- MetBrewer::met.brewer("Egypt")

theme_nice <- function() {
  theme_minimal(base_family = "Times") +
    theme(
      panel.grid.minor = element_blank(),
      plot.background = element_rect(fill = "white", color = NA),
      plot.title = element_textbox_simple(lineheight = 0.5, face = "bold"),
      plot.subtitle = element_textbox_simple(lineheight = 0.5),
      plot.caption = element_textbox_simple(lineheight = 0.5, halign = 1),
      axis.title = element_text(face = "bold"),
      strip.text = element_text(face = "bold", size = rel(0.8)),
      strip.background = element_rect(fill = "white", color = NA),
      legend.position = "bottom",
      legend.title=element_blank(),
      
      # Rien en X
      axis.title.x=element_blank(),
      axis.text.x=element_blank(),
      axis.ticks.x=element_blank())
}

# Faire de ce thème le thème par défaut
theme_set(theme_nice())
```

# Point de départ
Dans mon dernier billet de blog ([ici](https://charlymarie.github.io/posts/03%20-%20lm_summary()/lm_summary().html){target='_blank'}, qui date un peu, je sais), je proposais des fonctions pour automatiser l'extraction des coefficients d'un modèle de type `lm()` en `R`. Mais voilà, on peut potentiellement préférer un (joli) graphique, plutôt qu'une (jolie) table. Aussi jolie soit-elle. Voici donc une manière rapide, facile et personnalisable pour transformer des moyennes marginales en graphique, en `R`.

Je repars à nouveau du jeu de données `penguins` [@palmerpenguins], pour lequel j'ajuste le modèle suivant :

$$
\begin{aligned}
\text{masse}_i = \; & \alpha 
+ \beta_1 \, \mathbf{1}(\text{espèce}_i = \text{Jugulaire})
+ \beta_2 \, \mathbf{1}(\text{espèce}_i = \text{Papou}) \\
& + \beta_3 \, \mathbf{1}(\text{sexe}_i = \text{Mâle}) 
+ \beta_4 \, \mathbf{1}(\text{espèce}_i = \text{Jugulaire}) \times \mathbf{1}(\text{sexe}_i = \text{Mâle}) \\
& + \beta_5 \, \mathbf{1}(\text{espèce}_i = \text{Papou}) \times \mathbf{1}(\text{sexe}_i = \text{Mâle}) 
+ \varepsilon_i
\end{aligned}
$$ {#eq-un}

Chaque $\mathbf{1}(\cdots)$ est un indicateur qui prend 1 quand l'individu $i$ correspond à la catégorie, et 0 autrement. La catégorie de référence, lorsque tous les $\mathbf{1}(\cdots)$ valent 0, correspond aux femelles Adélie et $\alpha$ est leur masse moyenne.

Dit autrement, je régresse la masse sur l'espèce, le sexe, et l'interaction espèce x sexe. C'est une légère différence par rapport au dernier post, qui n'incluait pas d'interaction. La sortie est la suivante :

```{r, output=axis, echo=FALSE}
#| label: tbl-regression-lmsummary
#| tbl-cap: "Régression de la masse des manchots en grammes sur l'espèce, le sexe, et l'interaction espèce x sexe, selon l'@eq-un. Sortie de la fonction `lm_summary()` passée via `lm_tab()`."

df <- palmerpenguins::penguins %>%
  mutate(
    species = case_when(
      species == "Adelie" ~ "Adélie",
      species == "Chinstrap" ~ "Jugulaire",
      species == "Gentoo" ~ "Papou"),
    
    sex = case_when(
      sex == "female" ~ "Femelle",
      sex == "male" ~ "Mâle")) %>% # Traduire les données
  
  rename(
    masse = body_mass_g,
    espece = species,
    sexe = sex) # Renommer les colonnes

lm_1 <- lm(masse ~ espece * sexe, df)

lm_1 %>%
  lm_summary() %>%
  mutate(
    Paramètre = case_when(
      Paramètre == "especeJugulaire" ~ "Espèce : Jugulaire",
      Paramètre == "especePapou" ~ "Espèce : Papou",
      Paramètre == "sexeMâle" ~ "Sexe : Mâle",
      Paramètre == "especeJugulaire:sexeMâle" ~ "Espèce : Jugulaire x Sexe : Mâle",
      Paramètre == "especePapou:sexeMâle" ~ "Espèce : Papou x Sexe : Mâle",
      .default = Paramètre)) %>%
  lm_tab()
```

Cette table renvoie déjà toutes les informations nécessaires pour interpréter le modèle (si, si : on en parlera une prochaine fois). Mais, on pourrait malgré tout lui préférer une représentation graphique *directement estimée à partir du modèle*.

# Extraire les moyennes marginales et les envoyer vers un graphique `ggplot2`

Cela nécessite deux étapes : 

1. Extraire les moyennes marginales du modèle.
2. Représenter ces moyennes marginales.

## Extraire les moyennes marginales du modèle

(Presque) rien de plus simple, via le package `marginaleffects` [@marginaleffects] et sa fonction `avg_predictions()` :

```{r, output="asis"}
#| label: tbl-marginalmeans1
#| tbl-cap: "Régression de la masse des manchots en grammes, sur l'espèce et le sexe, selon l'@eq-un. Sortie de la fonction `avg_predictions()` passée via `knitr::kable()`."

lm_1_mean <- avg_predictions(lm_1, variables = c("espece", "sexe"), df = insight::get_df(lm_1))

lm_1_mean %>%
  knitr::kable()
```

Cette table ne fournit aucune information supplémentaire comparée à la table de régression^[L'`intercept` renvoie déjà la masse moyenne, pour un manchot de l'espèce de référence Adélie, de sexe de référence Femelle; le coefficient de `Espèce : Jugulaire` renvoie déjà la différence moyenne, pour un manchot de l'espèce Jugulaire, de sexe femelle, par rapport à l'intercept; etc...).], mais va nous permettre de réaliser notre graphique des moyennes marginales.

## Représenter les moyennes marginales du modèle

Ici, il nous suffit de repartir de l'objet qui stocke les moyennes marginales, pour l'envoyer vers `ggplot()` [@ggplot2wickham]. Cet objet contient, parmis d'autres, les deux informations qui nous intéressent ici :

-   La masse moyenne, par espèce, en fonction du sexe.
-   L'intervalle de confiance à 95 % de cette masse moyenne.

Je vous propose deux solutions, selon vos préférences. Aucun n'est vraiment mieux que l'autre :

-   Le premier, à gauche, met l'accent sur la comparaison entre espèces, à sexe constant.
-   Le second, à droite, met l'accent sur la comparaison entre sexes, à espèce constante.

```{r}
#| label: fig-plot-penguins
#| fig-cap: "Masse des manchots en grammes, selon l’espèce et le sexe. La barre représente l'intervalle de confiance, à 95 %."
#| out-width: "80%"
#| fig-align: "center"

# Un premier graphique, qui représente les  données en deux sous-facettes selon le sexe
p1 <- lm_1_mean %>%
  
  ggplot() +
  aes(x = espece, y = estimate, fill = sexe) +
  geom_col() +
  geom_errorbar(aes(ymin = conf.low, ymax = conf.high), width = 0) +
  scale_fill_manual(values = c(Femelle = egypt[1], Mâle = egypt[2])) +
  facet_wrap(~ sexe) + # C'est ici que ça se passe, pour représenter les données en deux sous-facettes
  labs(
    y = "Moyennes marginales de la masse (g)",
    fill = "Sexe",
    title = "Masse des manchots, selon l’espèce et le sexe") +
  theme(axis.text.x = element_text())

# Un second graphique, qui représente les données côte à côte
p2 <- lm_1_mean %>%
  ggplot() +
  aes(x = espece, y = estimate, fill = sexe) +
  geom_col(position = position_dodge(width = 0.8), width = 0.8) + # C'est ici que ça se passe, via position_dodge(), pour représenter les données côte à côte
  geom_errorbar(aes(ymin = conf.low, ymax = conf.high),
    position = position_dodge(width = 0.8),
    width = 0.0) +
  scale_fill_manual(values = c(Femelle = egypt[1], `Mâle` = egypt[2])) +
  labs(
    y = "Moyennes marginales de la masse (g)",
    fill = "Sexe",
    title = "Masse des manchots, selon l’espèce et le sexe") +
  theme(axis.text.x = element_text())

ggarrange(
  p1, p2,
  common.legend = T,
  legend = "bottom")
```

Cette @fig-plot-penguins ne dit rien de plus ou de moins que la @tbl-regression-lmsummary, mais le fait différemment, en étant certainement plus simple à lire qu'une table de régression.

# Références