HOWTO · Matplotlib

Comment tracer deux histogrammes ensemble dans Matplotlib

Cet article pratique présente la façon de tracer deux histogrammes en même temps dans Matplotlib.

Sur cette page

Pour tracer deux histogrammes ensemble dans Matplotlib, transmettez les deux jeux de données à plt.hist([a, b], bins=...) afin d’obtenir des barres côte à côte, ou appelez deux fois plt.hist() sur les mêmes axes pour superposer les barres. Utilisez des sous-graphiques distincts si vous souhaitez afficher chaque distribution dans son propre panneau. Quelle que soit l’approche, utilisez des bords de classes communs afin que les barres correspondantes représentent les mêmes intervalles.

Les exemples ci-dessous nécessitent NumPy et Matplotlib. Ils sont autonomes. L’affichage d’un graphique avec plt.show() nécessite un environnement de tracé interactif. Dans un environnement non interactif, enregistrez la figure comme indiqué dans la dernière section.

Tracer deux histogrammes sans chevauchement des barres

Transmettez une liste contenant les deux jeux de données à un seul appel de plt.hist(). Avec histtype="bar" et stacked=False, Matplotlib place les barres des deux jeux de données côte à côte dans chaque classe. Cette disposition est utile pour comparer les effectifs intervalle par intervalle sur un même système d’axes.

Lors de sa première exécution, Matplotlib peut signaler l’initialisation du cache des polices. La configuration de journalisation affiche les diagnostics du gestionnaire de polices sur la sortie standard plutôt que sur la sortie d’erreur ; elle ne les masque pas.

Cet exemple utilise des échantillons générés et comprend une légende :

import logging
import sys

# Keep first-run font-cache diagnostics visible on standard output.
font_logger = logging.getLogger("matplotlib.font_manager")
font_logger.addHandler(logging.StreamHandler(sys.stdout))
font_logger.propagate = False

import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(42)
a = rng.normal(0, 3, 3000)
b = rng.normal(2, 4, 2000)
bins = np.linspace(min(a.min(), b.min()), max(a.max(), b.max()), 21)

plt.figure(figsize=(8, 4))
plt.hist([a, b], bins=bins, histtype="bar", stacked=False, label=["a", "b"])
plt.xlabel("Value")
plt.ylabel("Count")
plt.title("Two histograms with grouped bars")
plt.legend(loc="upper left")
plt.tight_layout()
plt.show()

Deux histogrammes avec des barres groupées : les barres bleues du jeu de données a et les barres orange du jeu de données b se tiennent côte à côte dans chaque classe, de sorte que leurs effectifs peuvent être comparés classe par classe.

Ici, np.linspace() fournit 21 bords, ce qui produit 20 classes sur l’étendue combinée des données. Augmentez le nombre de bords pour faire apparaître davantage de détails ; diminuez-le pour mettre en évidence les tendances générales. Il est également possible de fixer une étendue, par exemple avec np.linspace(-10, 10, 21), mais les observations situées en dehors de cette étendue ne seront pas incluses dans l’histogramme.

Un histogramme ne nécessite pas d’échantillons distribués selon une loi normale. Remplacez a et b par vos propres tableaux numériques unidimensionnels de mesures, exprimées dans des unités cohérentes. Convertissez les listes avec np.asarray() avant d’utiliser les appels .min() et .max() présentés ci-dessus. Éliminez les valeurs manquantes et non finies, ou traitez-les, avant de déduire les bords des classes à partir des données. Les deux tableaux doivent contenir des observations pour que les calculs du minimum et du maximum présentés ici fonctionnent. Si toutes les mesures ont la même valeur, indiquez des bords distincts et croissants qui englobent cette valeur au lieu de calculer des bords identiques.

Tracer deux histogrammes avec chevauchement des barres

Appelez deux fois plt.hist() sur les mêmes axes pour superposer les barres. Transmettez le même tableau de bords aux deux appels. Si vous indiquez seulement bins=20 séparément, chaque appel calculera ses propres bords à partir de l’étendue de ses données ; les intervalles pourraient donc différer malgré le même nombre de classes.

import logging
import sys

# Keep first-run font-cache diagnostics visible on standard output.
font_logger = logging.getLogger("matplotlib.font_manager")
font_logger.addHandler(logging.StreamHandler(sys.stdout))
font_logger.propagate = False

import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(42)
a = rng.normal(0, 3, 1000)
b = rng.normal(2, 4, 900)
bins = np.linspace(min(a.min(), b.min()), max(a.max(), b.max()), 51)

plt.figure(figsize=(8, 4))
plt.hist(a, bins=bins, alpha=0.5, label="a", color="tab:blue")
plt.hist(b, bins=bins, alpha=0.5, label="b", color="tab:orange")
plt.xlabel("Value")
plt.ylabel("Count")
plt.title("Two histograms with overlapping bars")
plt.legend(loc="upper left")
plt.tight_layout()
plt.show()

Deux histogrammes superposés avec des barres transparentes : là où les barres bleues du jeu de données a et les barres orange du jeu de données b se chevauchent, les couleurs se mélangent, de sorte que les deux distributions restent visibles.

Le paramètre alpha règle l’opacité : 0.0 signifie totalement transparent, 1.0 totalement opaque et 0.5 permet aux deux couleurs de contribuer aux zones où les barres se chevauchent. Si vous omettez alpha, les barres sont normalement opaques ; elles ne deviennent pas transparentes. Lorsque des barres opaques ont le même zorder, l’histogramme tracé en second peut masquer le premier partout où leurs rectangles se chevauchent, quel que soit l’effectif le plus élevé.

Les mêmes deux échantillons sans alpha montrent pourquoi la transparence compte. L’histogramme orange est tracé en second et masque le bleu jusqu’à sa propre hauteur partout où ils se chevauchent ; là où une barre orange est plus haute, la barre bleue derrière elle disparaît complètement, même lorsqu’elle représente davantage d’observations.

Les mêmes deux histogrammes sans transparence : les barres orange opaques tracées en second masquent les barres bleues derrière elles partout où la barre orange est au moins aussi haute.

Les effectifs dépendent aussi de la taille des échantillons. Si les groupes contiennent des nombres d’observations différents et que vous souhaitez comparer la forme des distributions, définissez density=True dans les deux appels et nommez l’axe vertical "Probability density". Chaque histogramme a alors une aire totale de un pour les observations qu’il inclut ; la somme des hauteurs des barres n’est pas nécessairement égale à un.

Comparer des histogrammes dans des sous-graphiques côte à côte

Des panneaux distincts évitent qu’un histogramme en masque un autre et facilitent l’examen de chaque distribution. Conservez les mêmes classes et les mêmes échelles d’axes afin que les différences d’échelle entre les panneaux ne faussent pas la comparaison.

import logging
import sys

# Keep first-run font-cache diagnostics visible on standard output.
font_logger = logging.getLogger("matplotlib.font_manager")
font_logger.addHandler(logging.StreamHandler(sys.stdout))
font_logger.propagate = False

import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(42)
data1 = rng.normal(0, 1, 1000)
data2 = rng.normal(1, 1, 1000)
bins = np.linspace(min(data1.min(), data2.min()), max(data1.max(), data2.max()), 31)

plt.figure(figsize=(9, 4))
left = plt.subplot(1, 2, 1)
plt.hist(data1, bins=bins, color="tab:blue")
plt.title("Dataset 1")
plt.xlabel("Value")
plt.ylabel("Count")

plt.subplot(1, 2, 2, sharex=left, sharey=left)
plt.hist(data2, bins=bins, color="tab:orange")
plt.title("Dataset 2")
plt.xlabel("Value")
plt.ylabel("Count")

plt.tight_layout()
plt.show()

Deux histogrammes dans des panneaux distincts côte à côte avec des axes partagés : le jeu de données 1 à gauche et le jeu de données 2 à droite utilisent les mêmes classes et les mêmes échelles.

Dans plt.subplot(1, 2, 1), les deux premiers arguments définissent une ligne et deux colonnes ; le troisième sélectionne le panneau de gauche. La position 2 sélectionne celui de droite. Le partage des deux axes maintient leurs échelles alignées, tandis que plt.tight_layout() ajuste l’espacement autour des titres et des libellés. Contrairement aux barres côte à côte, cette disposition utilise deux axes dans une même figure.

Déduire les bords de classes en toute sécurité lorsqu’un jeu de données peut être vide

Les exemples ci-dessus déduisent les bords de classes partagés à partir du minimum et du maximum des deux jeux de données. Cette déduction échoue lorsqu’un jeu de données est vide, car un tableau vide n’a pas de minimum :

import numpy as np
import matplotlib.pyplot as plt

a = np.array([])
b = np.array([1.0, 2.0, 3.0])
bins = np.linspace(min(a.min(), b.min()), max(a.max(), b.max()), 21)
plt.hist([a, b], bins=bins)

Sortie:

Traceback (most recent call last):
  ...
ValueError: zero-size array to reduction operation minimum which has no identity

Filtrez les jeux de données vides avant de déduire les bords. Éliminez les valeurs non finies à la même étape : un seul NaN rend le minimum et le maximum égaux à NaN, de sorte que chaque bord déduit est NaN et que l’histogramme n’a aucune classe utilisable.

import numpy as np

a = np.array([])
b = np.array([1.0, 2.0, 3.0])
datasets = [d for d in (a, b) if d.size > 0]
bins = np.linspace(min(d.min() for d in datasets), max(d.max() for d in datasets), 21)
print("datasets kept:", len(datasets))
print("first and last bin edges:", bins[0], bins[-1])

Sortie:

datasets kept: 1
first and last bin edges: 1.0 3.0

Une fois le jeu de données vide filtré, le jeu de données restant détermine les bords et peut être tracé avec les appels à barres groupées ou superposées des sections précédentes. Si datasets est vide après le filtrage, il n’y a rien à tracer ; gérez ce cas dans votre propre code au lieu de déduire des bords.

Personnaliser et légender les histogrammes

Attribuez à chaque jeu de données un libellé descriptif dans la légende et une couleur distincte. Utilisez edgecolor pour tracer le contour des barres, puis linestyle pour différencier davantage leurs contours. Des lignes de grille horizontales discrètes aident à estimer les hauteurs sans prendre le dessus sur la comparaison.

import logging
import sys

# Keep first-run font-cache diagnostics visible on standard output.
font_logger = logging.getLogger("matplotlib.font_manager")
font_logger.addHandler(logging.StreamHandler(sys.stdout))
font_logger.propagate = False

import numpy as np
import matplotlib.pyplot as plt

rng = np.random.default_rng(42)
data1 = rng.normal(0, 1, 1000)
data2 = rng.normal(1, 1, 1000)
bins = np.linspace(min(data1.min(), data2.min()), max(data1.max(), data2.max()), 31)

fig, ax = plt.subplots(figsize=(8, 4))
ax.hist(data1, bins=bins, color="tab:blue", edgecolor="black",
        linestyle="--", alpha=0.5, label="Dataset 1")
ax.hist(data2, bins=bins, color="tab:orange", edgecolor="black",
        linestyle=":", alpha=0.5, label="Dataset 2")
ax.set(title="Styled histogram comparison", xlabel="Value", ylabel="Count")
ax.set_axisbelow(True)
ax.grid(axis="y", alpha=0.3)
ax.legend()
fig.tight_layout()
plt.show()

Deux histogrammes superposés stylisés : le jeu de données 1 a des contours de barres en tirets et le jeu de données 2 des contours en pointillés, avec de discrètes lignes de grille horizontales derrière les barres.

Pour mettre en évidence les formes sans remplir les barres, utilisez histtype="step" et choisissez une valeur adaptée pour linewidth dans chaque appel de l’histogramme. Lorsque vous adaptez ces exemples, indiquez les unités de mesure sur le libellé de l’axe horizontal.

Pour enregistrer un graphique, insérez fig.savefig("histograms.png", dpi=150, bbox_inches="tight") avant plt.show() dans le dernier exemple. Pour les exemples utilisant pyplot, appelez plt.savefig() avant d’afficher la figure. Enregistrer avant l’affichage évite de dépendre du comportement du moteur de tracé après l’affichage ; l’enregistrement fonctionne aussi sans fenêtre d’affichage.

Pour représenter plus de deux jeux de données, ajoutez-les à la liste transmise à un seul appel de plt.hist() et fournissez un libellé pour chacun, ou ajoutez des appels en utilisant les mêmes bords. Quand le nombre de groupes augmente, des contours ou des panneaux distincts restent souvent plus lisibles que de nombreux aplats transparents.