
Cet article fait suite à un retour d’un lecteur concernant un comportement de dplyr souvent mal compris avec les fonctions summarise() et group_by().
Vous exécutez votre code. Tout fonctionne. Les résultats semblent cohérents.
Et pourtant… ils sont faux !
Parce qu’ une partie du traitement n’est en réalité pas maîtrisée.
C’est typiquement le genre de point sur lequel j’insiste dans mes formations : comprendre précisément ce que fait le code pour éviter des erreurs silencieuses dans les analyses.
Pour illustrer ce comportement dans un contexte plus proche de la pratique, nous allons utiliser un jeu de données heart_disease. issu du package funModeling :
# chargement du packahe funModeling
library(funModeling)
# chargement des données
data("heart_disease")
# chargement du package dplyr
library(dplyr)
# aperçu des données
glimpse(heart_disease)
Rows: 303
Columns: 16
$ age <int> 63, 67, 67, 37, 41, 56, 62, 57, 63, 53, 57, 56,…
$ gender <fct> male, male, male, male, female, male, female, f…
$ chest_pain <fct> 1, 4, 4, 3, 2, 2, 4, 4, 4, 4, 4, 2, 3, 2, 3, 3,…
$ resting_blood_pressure <int> 145, 160, 120, 130, 130, 120, 140, 120, 130, 14…
$ serum_cholestoral <int> 233, 286, 229, 250, 204, 236, 268, 354, 254, 20…
$ fasting_blood_sugar <fct> 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0,…
$ resting_electro <fct> 2, 2, 2, 0, 2, 0, 2, 0, 2, 2, 0, 2, 2, 0, 0, 0,…
$ max_heart_rate <int> 150, 108, 129, 187, 172, 178, 160, 163, 147, 15…
$ exer_angina <int> 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0,…
$ oldpeak <dbl> 2.3, 1.5, 2.6, 3.5, 1.4, 0.8, 3.6, 0.6, 1.4, 3.…
$ slope <int> 3, 2, 2, 3, 1, 1, 3, 1, 2, 3, 2, 2, 2, 1, 1, 1,…
$ num_vessels_flour <int> 0, 3, 2, 0, 0, 0, 2, 0, 1, 0, 0, 0, 1, 0, 0, 0,…
$ thal <fct> 6, 3, 7, 3, 3, 3, 3, 3, 7, 7, 6, 3, 6, 7, 7, 3,…
$ heart_disease_severity <int> 0, 2, 1, 0, 0, 0, 3, 0, 2, 1, 0, 0, 2, 0, 0, 0,…
$ exter_angina <fct> 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0,…
$ has_heart_disease <fct> no, yes, yes, no, no, no, yes, no, yes, yes, no… Ce jeu de données contient des informations cliniques classiques, notamment :
gender)chest_pain)resting_blood_pressure)
Afin de faciliter l’interprétation des résultats, les modalités de la variable chest_pain sont recodées avec des libellés explicites :
# chargement du package forcast pour recoder les modalités de la douleur
library(forcats)
heart_disease$chest_pain<- heart_disease$chest_pain %>%
fct_recode(
"Typical Angina" = "1",
"Atypical Angina" = "2",
"Non-anginal pain" = "3",
"Asymptomatic" = "4"
)
# Affichage des 5 premières lignes des données
heart_disease |>
slice_head(n=5)
age gender chest_pain resting_blood_pressure serum_cholestoral
1 63 male Typical Angina 145 233
2 67 male Asymptomatic 160 286
3 67 male Asymptomatic 120 229
4 37 male Non-anginal pain 130 250
5 41 female Atypical Angina 130 204
fasting_blood_sugar resting_electro max_heart_rate exer_angina oldpeak slope
1 1 2 150 0 2.3 3
2 0 2 108 1 1.5 2
3 0 2 129 1 2.6 2
4 0 0 187 0 3.5 3
5 0 2 172 0 1.4 1
num_vessels_flour thal heart_disease_severity exter_angina has_heart_disease
1 0 6 0 0 no
2 3 3 2 1 yes
3 2 7 1 1 yes
4 0 3 0 0 no
5 0 3 0 0 no Dans la suite, nous allons nous intéresser à la pression artérielle au repos par sexe et type de douleur
Imaginons que vous souhaitiez comparer la pression artérielle au repos selon le sexe et le type de douleur de poitrine.
On peut alors calculer la moyenne de resting_blood_pressure pour chaque combinaison de genderet de chest_pain , en utilisant les fonctions group_by() et summarise() du package dplyr , comme ceci :
# pour obtenir un affichage des moyennes avec une décimale après la virgule
options(pillar.sigfig = 4)
# calcul des moyennes par combinaison des modalités gender et chest pain
by_gender_chest_pain <- heart_disease |>
group_by(gender, chest_pain) |>
summarise(
mean_resting_blood_pressure = round(mean(resting_blood_pressure, na.rm = TRUE), 1))
# affichage du résultat
by_gender_chest_pain
# A tibble: 8 × 3
# Groups: gender [2]
gender chest_pain mean_resting_blood_pressure
<fct> <fct> <dbl>
1 female Typical Angina 147.5
2 female Atypical Angina 128.1
3 female Non-anginal pain 127.9
4 female Asymptomatic 139.1
5 male Typical Angina 139.5
6 male Atypical Angina 128.6
7 male Non-anginal pain 131.9
8 male Asymptomatic 129.6 À ce stade, le résultat semble correspondre à ce que l’on attend : une ligne par combinaison de sexe et de type de douleur thoracique.
Autrement dit, dplyr a bien calculé la pression artérielle moyenne au repos pour chaque sous-groupe.
Mais le piège n’est pas dans le tableau obtenu.
Il apparaît juste après, lorsque l’on réutilise ce tableau pour poursuivre l’analyse.
Jusqu’ici, tout semble correct.
Mais, imaginons maintenant que vous souhaitiez identifier les deux groupes présentant la pression artérielle moyenne au repos la plus élevée.
Vous pourriez écrire :
by_gender_chest_pain |>
slice_max(mean_resting_blood_pressure, n = 2)
# A tibble: 4 × 3
# Groups: gender [2]
gender chest_pain mean_resting_blood_pressure
<fct> <fct> <dbl>
1 female Typical Angina 147.5
2 female Asymptomatic 139.1
3 male Typical Angina 139.5
4 male Non-anginal pain 131.9 Intuitivement, vous vous attendez à obtenir 2 lignes.
Mais en réalité, vous en obtenez… 4.
Ce comportement s’explique par le fait que le tableau by_gender_chest_pain reste groupé après l’appel à summarise().
Plus précisément :
gender et chest_painsummarise(), le résultat reste groupégenderCe comportement n’est pas complètement intuitif…..
De la même façon, si vous aviez utilisé 3 variables dans group_by(), le tableau obtenu après summarise()serait resté groupé selon les 2 premières variables.
Au final, l’instruction placée après summarise() est appliquée à l’intérieur des groupes résiduels, et non sur l’ensemble du tableau.
Dans notre exemple, slice_max()est donc appliqué séparément :
C’est pour cette raison que vous obtenez 4 lignes au lieu de 2.
En réalité, R vous avertit avec le message suivant affiché dans la console :

Mais il est fréquent de ne pas voir ce message, ou de ne pas comprendre immédiatement ce qu’il implique pour la suite de l’analyse.
Ce comportement est piégeux parce que R vous alerte effectivement, mais sans interrompre l’exécution du code. Le script continue donc à tourner, le tableau est produit, et si vous ne prêtez pas attention au message dans la console – ou si vous ne comprenez pas exactement ce qu’il signifie – vous pouvez facilement passer à côté du problème.
Ici, l’erreur est visible parce que vous attendiez 2 lignes et que vous en obtenez 4. Mais dans une analyse plus complexe, ce type de décalage peut être beaucoup moins évident.
Et surtout, le problème ne se limite pas au nombre de lignes obtenu.
Imaginons par exemple que, dans la suite de votre code, vous décidiez de conserver uniquement les deux premières lignes du tableau obtenu. Vous pourriez penser récupérer les deux groupes présentant les pressions artérielles moyennes les plus élevées.
Mais en réalité, ce ne serait pas le cas.
Dans notre exemple, le groupe “female – Typical Angina” possède effectivement la valeur la plus élevée. En revanche, la deuxième ligne du tableau ne correspond pas au deuxième groupe ayant la pression artérielle moyenne la plus élevée dans l’ensemble des données. Elle correspond simplement au deuxième groupe sélectionné à l’intérieur du groupe female.
Autrement dit, le regroupement résiduel modifie subtilement la logique de sélection, et peut conduire à retenir des groupes qui ne sont pas ceux que vous pensiez identifier.
C’est précisément ce qui rend ce type de comportement dangereux : le code fonctionne, le résultat semble plausible, mais l’analyse réalisée n’est pas celle que vous aviez en tête.
Il existe plusieurs manières d’éviter ce comportement. L’idée générale est toujours la même : expliciter ce que vous voulez faire des groupes après le calcul des statistiques descriptives.
La première solution, qui a longtemps été la manière classique de procéder avec dplyr, consiste à supprimer explicitement le regroupement après summarise() , en utilisant une fonction ungroup()
by_gender_chest_pain <- heart_disease |>
group_by(gender, chest_pain) |>
summarise(
mean_resting_blood_pressure = round(mean(resting_blood_pressure, na.rm = TRUE), 1)
) |>
ungroup() # utilisation de la fonction ungroup() Vous pouvez ensuite appliquer slice_max() sur l’ensemble du tableau :
by_gender_chest_pain |>
slice_max(mean_resting_blood_pressure, n = 2)
# A tibble: 2 × 3
gender chest_pain mean_resting_blood_pressure
<fct> <fct> <dbl>
1 female Typical Angina 147.5
2 male Typical Angina 139.5 Cette fois, vous obtenez bien les deux groupes ayant les pressions artérielles moyennes au repos les plus élevées, tous groupes confondus.
Une autre solution, apparue ensuite dans dplyr, consiste à indiquer directement dans summarise() que vous ne souhaitez conserver aucun regroupement, en employant l’argument .groups = "drop":
by_gender_chest_pain <- heart_disease |>
group_by(gender, chest_pain) |>
summarise(
mean_resting_blood_pressure = round(mean(resting_blood_pressure, na.rm = TRUE), 1),
.groups = "drop" # ici
) L’avantage est que le comportement est explicite dès l’étape de résumé. Vous évitez ainsi de transporter un regroupement résiduel dans la suite de votre analyse.
by_gender_chest_pain |>
slice_max(mean_resting_blood_pressure, n = 2)
# A tibble: 2 × 3
gender chest_pain mean_resting_blood_pressure
<fct> <fct> <dbl>
1 female Typical Angina 147.5
2 male Typical Angina 139.5 Plus récemment encore, dplyr a introduit l’argument .by, qui permet d’effectuer le regroupement directement danssummarise(), sans avoir besoin d’utiliser group_by() en amont, comme ceci :
by_gender_chest_pain <- heart_disease |>
summarise(
mean_resting_blood_pressure = round(mean(resting_blood_pressure, na.rm = TRUE), 1),
.by = c(gender, chest_pain) # ici
)
gender chest_pain mean_resting_blood_pressure
1 female Typical Angina 147.5
2 male Typical Angina 139.5 C’est peut être la solution la plus lisible, car elle rend le code plus local : vous voyez directement, dans l’appel à summarise(), les variables utilisées pour calculer les statistiques par groupe.
Personnellement j’utilise la solution 2.
Ce type de comportement peut sembler anecdotique… mais il illustre parfaitement une réalité fréquente en analyse de données : un code qui s’exécute sans erreur n’est pas forcément un code dont le résultat est maîtrisé.
Dans cet exemple, le piège vient du fait que le regroupement est conservé de manière implicite après summarise()mais uniquement sur les n - 1 premières variables utilisées dans group_by()
Sans une bonne compréhension de ce fonctionnement, il est facile d’enchaîner des opérations qui ne produisent pas les résultats attendus.
C’est précisément pour cela qu’il est important de ne pas se contenter de faire “tourner” du code, mais de comprendre finement ce que chaque étape produit.
Enfin, si vous avez déjà été confronté à ce type de comportement, ou si vous avez d’autres exemples de situations “piégeuses” avec dplyr, n’hésitez pas à les partager en commentaire.

CAMPUS DELLADATA
Je propose désormais une formation en ligne pour démarrer avec R et RStudio, pensée pour les profils scientifiques (recherche médicale, biologie, agro, environnement…). D’autres modules arrivent prochainement.

Je suis Claire Della-Vedova, consultante en biostatistique, méthodologie clinique et expertise R.
J’accompagne les fabricants de dispositifs médicaux et les équipes scientifiques des sciences du vivant dans leurs projets d’évaluation clinique, d’analyse statistique et d’analyse de données sous R.
🎓 Formations professionnelles R et biostatistiques
🤝 Prestations et accompagnement sur mesure
📅 Discuter d’un accompagnement ou d’une prestation sur mesure

Enregistrez vous pour recevoir gratuitement mes fiches « aide mémoire » (ou cheat sheets) qui vous permettront de réaliser facilement les principales analyses biostatistiques avec le logiciel R et pour être informés des mises à jour du site.
4 réponses
Merci. J’utilisais la solution 2 mais sans avoir réellement remarqué le danger. Je le fais intuitivement tant affectueusement peur que le grouping se propage au cours de l’analyse. Je n’avais même pas encore remarqué le message d’attention de R.
Très utile, merci beaucoup, personnellement j’utilse souvent la solution 1, mais je après la lecture de cet article je pense que je vais migrer à la solution 3
Bravo Claire pour ce tuto qui rappelle les écueils rencontrés très tôt dans des études de cas.
Bravo 👏 pour cette demonstration pas a pas.