Superposer une densité lissée à un histogramme d’effectifs — avec l’astuce de l’axe y secondaire (effectifs à gauche, densité à droite)
Data Visualization
Comment ajouter une courbe de densité lissée à un histogramme ggplot2 en R — à la fois la méthode simple (placer l’histogramme sur l’échelle de densité avec after_stat(density)) et la méthode de l’axe y secondaire qui conserve les effectifs bruts sur l’axe de gauche et la densité à droite via sec_axis(). Chaque graphique est rendu à partir de vrai code R prêt à copier, en n’utilisant que ggplot2.
La méthode la plus simple pour superposer une courbe de densité à un histogramme — placer les deux sur l’échelle de densité avec after_stat(density).
La technique de l’axe y secondaire : conserver les effectifs bruts à gauche et la densité à droite, comme le fait la recette d’origine « histogramme avec courbe de densité » — mais avec uniquement ggplot2 de base (sans ggpubr/cowplot).
Le seul nombre qui rend l’opération possible : le facteur d’échelle = largeur de classe × n.
Chaque graphique est rendu à partir de vrai code R que vous pouvez copier et exécuter — vous n’avez besoin que de ggplot2.
Un histogramme vous montre où vos données s’accumulent ; une courbe de densité en montre la forme lissée. Réunir les deux constitue l’un des graphiques de diagnostic les plus utiles en R — mais il y a un piège. Un histogramme d’effectifs et une courbe de densité vivent sur des échelles différentes (effectifs vs densité de probabilité), si bien que vous ne pouvez pas les empiler naïvement. Il existe deux façons propres de régler cela, et ce billet couvre les deux.
Nous utiliserons le jeu de données intégré faithful (durées d’éruption du geyser Old Faithful, en minutes) — sa célèbre forme bimodale fait gagner sa place à la courbe de densité.
Le problème d’échelle en une phrase
geom_histogram() trace par défaut des effectifs (la hauteur des barres correspond au nombre d’observations qui tombent dans chaque classe), tandis que geom_density() trace une densité de probabilité (l’aire sous la courbe a une intégrale égale à 1). Sur les données d’éruption, les effectifs se comptent par dizaines alors que la densité culmine en dessous de 1 — superposez-les telles quelles et la courbe de densité s’aplatit le long de l’axe des x. Tout ce qui suit n’est que deux façons de concilier ces échelles.
La méthode simple — placer les deux sur l’échelle de densité
La correction la plus propre consiste à cesser de tracer des effectifs et à tracer la densité à la place, de sorte que l’histogramme et la courbe partagent un même axe y. Pour cela, on associe le y de l’histogramme à la statistique calculée after_stat(density) :
Désormais les deux couches sont dans les mêmes unités (densité), si bien que la courbe épouse exactement le sommet des barres. C’est le bon choix la plupart du temps — c’est honnête, cela ne demande qu’une ligne de code supplémentaire, et l’axe y a du sens. La forme bimodale est sans équivoque.
after_stat(density) remplace l’ancienne syntaxe ..density.. (dépréciée dans les versions récentes de ggplot2). Si vous voyez encore aes(y = ..density..) dans d’anciens tutoriels, remplacez-le par after_stat(density).
Quand vous voulez les effectifs bruts : l’axe y secondaire
Parfois vous voulez précisément que l’histogramme continue d’afficher des effectifs — c’est ce que les lecteurs s’attendent à lire sur un histogramme — tout en montrant la courbe de densité. L’astuce consiste à :
Tracer normalement l’histogramme d’effectifs.
Mettre la courbe de densité à l’échelle en la multipliant par une constante pour qu’elle se place au sommet des barres d’effectifs.
Ajouter un axe secondaire qui divise par cette même constante, de sorte que l’axe de droite se lise en véritables unités de densité.
La constante magique est le facteur d’échelle qui convertit la densité en effectifs : pour un histogramme, count ≈ density × binwidth × n. Calculez-le une fois en R pur :
binwidth <-0.25n <-nrow(faithful)scale_factor <- binwidth * n # density * scale_factor -> count scalescale_factor
[1] 68
Associez ensuite la courbe de densité à l’échelle des effectifs avec after_stat(density) * scale_factor, et laissez sec_axis() construire un axe de droite qui annule cette opération :
Lisez l’axe de gauche pour les effectifs et l’axe de droite pour la densité — même image, deux échelles honnêtes, et aucun package supplémentaire. C’est le remplacement moderne, en ggplot2 de base, de l’ancienne recette de superposition à deux graphiques ggpubr + cowplot : un seul appel à ggplot(), un seul sec_axis().
Note
Gardez binwidth synchronisé. Le facteur d’échelle est lié à la binwidth et au n de l’histogramme. Si vous changez binwidth, recalculez scale_factor (sinon la courbe ne s’alignera pas avec les barres). Vous utilisez bins = 30 à la place ? Alors binwidth = diff(range(x)) / 30 — il est plus propre de définir explicitement binwidth.
Une version soignée avec une ligne de moyenne
Une petite amélioration qui donne au graphique l’allure d’une figure aboutie : teinter les barres un peu plus clair, remplir sous la courbe de densité et marquer la moyenne d’une ligne en tirets.
Pour comparer des groupes, colorez les histogrammes selon la variable de groupe et donnez à chacun sa propre courbe de densité — effectifs sur l’axe de gauche, densité à droite, avec une ligne de moyenne en tirets et un tapis par groupe. L’unique axe secondaire fonctionne proprement lorsque les groupes partagent le même n (de sorte qu’une seule correspondance density = count / (binwidth × n) convient aux deux) ; avec des tailles de groupe très inégales, préférez plutôt le facettage.
library(ggplot2)set.seed(123)wdata <-data.frame(sex =factor(rep(c("F", "M"), each =200)),weight =c(rnorm(200, mean =55, sd =1.4), rnorm(200, mean =58, sd =1.4)))bw <-0.4n_per_group <-200means <-aggregate(weight ~ sex, data = wdata, FUN = mean)ggplot(wdata, aes(x = weight, fill = sex, colour = sex)) +geom_histogram(binwidth = bw, position ="identity", alpha =0.45, colour ="white") +geom_density(aes(y =after_stat(count) * bw), linewidth =0.9, fill =NA) +geom_vline(data = means, aes(xintercept = weight, colour = sex),linetype ="dashed", linewidth =0.7, show.legend =FALSE) +geom_rug(alpha =0.4) +scale_y_continuous(name ="Count",sec.axis =sec_axis(~ . / (bw * n_per_group), name ="Density")) +scale_fill_manual(values =c("#3a86d4", "#f5a524")) +scale_colour_manual(values =c("#3a86d4", "#f5a524")) +labs(x ="Weight", fill ="Sex", colour ="Sex") +theme_minimal(base_size =13)
Chaque groupe conserve son propre histogramme, sa courbe de densité et sa ligne de moyenne. L’axe secondaire est honnête ici parce que les deux groupes ont le même nombre d’observations — le même facteur count → density s’applique aux deux.
Laquelle devriez-vous utiliser ?
Par défaut, optez pour la superposition sur l’échelle de densité (after_stat(density) sur les deux). C’est le graphique à axe unique le plus honnête, et la courbe s’aligne sur les barres par construction.
N’utilisez l’axe secondaire que lorsque votre public a spécifiquement besoin de lire les effectifs bruts sur l’histogramme tout en voyant la forme lissée — et étiquetez toujours les deux axes pour que personne ne confonde une échelle avec l’autre.
Pour des groupes (deux distributions à comparer), l’axe secondaire fonctionne encore lorsque les groupes partagent le même n (un seul facteur count → density convient aux deux) — colorez par groupe avec un remplissage transparent. Avec des tailles de groupe très inégales, facettez plutôt, ou utilisez l’échelle de densité, afin qu’un axe ne puisse pas être mal interprété d’un groupe à l’autre.
Pour aller plus loin dans /learn
Ce billet est la recette ciblée. Pour les leçons complètes, pas à pas — classes, noyaux, regroupement, thématisation et les parcours soumis au contrôle de reproductibilité — voir :
Demandez à Prova« ajoute une courbe de densité à mon histogramme sur un axe secondaire et étiquette les deux axes » — elle répond avec du code que vous pouvez exécuter sur vos propres données. The runtime is the judge.Demander à Prova →
Cette page vous a-t-elle été utile ?
Merci pour votre retour !
Recevez les nouvelles leçons R & Python par e-mail
Pratique, reproductible, sans spam. Désinscription à tout moment.
Double opt-in. Nous ne partageons jamais votre e-mail.
@online{kassambara2026,
author = {Kassambara, Alboukadel},
title = {Histogramme avec courbe de densité en R (ggplot2)},
date = {2026-06-21},
url = {https://www.datanovia.com/blog/histogram-density-curve},
langid = {fr}
}