ITSkillsCenter
Blog

Analyse exploratoire avec pandas et matplotlib : tutoriel pas à pas

16 دقائق للقراءة

L’analyse exploratoire est la première étape sérieuse de tout projet de science des données. Elle consiste à ouvrir un jeu de données inconnu, à comprendre ce qu’il contient, à repérer les anomalies et à formuler les premières hypothèses. C’est aussi l’étape qui révèle si les données récoltées peuvent réellement répondre à la question posée — ou s’il faut retourner voir le métier pour clarifier le besoin avant d’écrire la moindre ligne de modélisation.

Pour creuser ce sujet sur la stack et les choix d’outillage, voir le guide principal sur la stack data 2026. Ce tutoriel se concentre sur la mise en pratique avec pandas et matplotlib — depuis le chargement du fichier jusqu’à la production des premiers graphiques exploitables.

Prérequis

  • Python 3.10 ou supérieur
  • JupyterLab ou VS Code avec extension Jupyter
  • pandas 3.0+, matplotlib 3.10+, numpy
  • Connaissance de base de Python (boucles, fonctions, dictionnaires)
  • Temps estimé : 90 minutes pour le tutoriel complet

Le jeu de données fil rouge

Pour rendre le tutoriel concret, on travaille sur un cas réaliste : un export de transactions issu d’une base e-commerce. Chaque ligne représente une commande avec ses caractéristiques (montant, date, produit, client, statut de paiement). Le fichier fait environ 50 000 lignes — assez pour que les agrégations soient parlantes, assez petit pour rester en mémoire sans difficulté. Si vous n’avez pas un tel fichier sous la main, le jeu de données Online Retail II du UCI Machine Learning Repository fait office d’équivalent libre.

Étape 1 — Préparer l’environnement

Avant tout, on isole le projet dans un environnement virtuel. Cette pratique évite que les versions installées entrent en conflit avec d’autres projets sur la même machine, et garantit qu’un collègue qui clone le code obtiendra exactement les mêmes versions de bibliothèques que vous. C’est aussi le moment de figer les dépendances dans un fichier requirements.txt que l’on versionnera avec le code.

python -m venv .venv
source .venv/bin/activate    # Linux/macOS
# .venv\Scripts\activate     # Windows PowerShell
pip install --upgrade pip
pip install pandas matplotlib numpy jupyterlab pyarrow

L’installation prend généralement entre 30 secondes et 2 minutes selon la connexion. Une fois terminée, lancer jupyter lab ouvre l’interface dans le navigateur sur le port 8888. Si Jupyter ne démarre pas et renvoie une erreur de port occupé, soit un autre serveur tourne déjà (jupyter lab list pour vérifier), soit on peut forcer un port libre avec jupyter lab --port=8889.

Étape 2 — Charger le fichier et inspecter sa forme

Le premier réflexe sur un fichier inconnu est de regarder sa structure : combien de lignes, combien de colonnes, quels types. La fonction read_csv de pandas accepte une dizaine de paramètres utiles dès le chargement (séparateur, encodage, types attendus). On commence cependant par un chargement minimal pour voir ce que pandas devine tout seul, puis on corrige si nécessaire.

import pandas as pd
import matplotlib.pyplot as plt

df = pd.read_csv("transactions.csv")
print(df.shape)        # (lignes, colonnes)
print(df.dtypes)       # type détecté pour chaque colonne
df.head(10)            # affiche les 10 premières lignes

Le résultat de shape donne la taille brute du fichier. Les dtypes révèlent souvent des surprises : une colonne date arrive en object (texte) plutôt qu’en datetime64, ou une colonne numérique se retrouve typée texte parce qu’une ligne contient une virgule au lieu d’un point. head permet de visualiser les premières lignes et de confirmer que le séparateur a été correctement détecté. Si les colonnes apparaissent fusionnées dans une seule, il faut spécifier explicitement le séparateur avec pd.read_csv("file.csv", sep=";").

Étape 3 — Profiler les valeurs manquantes et les doublons

Avant de plonger dans les graphiques, on évalue la qualité brute. Quelles colonnes ont des trous, et combien ? Y a-t-il des lignes en double ? Ces deux questions répondent en deux lignes de code, et leur réponse oriente toute la suite. Une colonne qui contient 80 % de valeurs manquantes ne sera probablement pas exploitable, ou alors elle nécessitera une stratégie spécifique (imputation, suppression, ou transformation en variable binaire « renseigné / non renseigné »).

# Pourcentage de valeurs manquantes par colonne
manquantes = df.isna().mean().sort_values(ascending=False) * 100
print(manquantes.round(1))

# Nombre de lignes en doublon strict
print("Doublons :", df.duplicated().sum())

La sortie ressemble à une liste triée des colonnes avec leur pourcentage de manques. Une colonne à 0 % est complète, une colonne à 100 % est vide et doit être supprimée immédiatement. Pour les colonnes intermédiaires (entre 5 % et 30 % de manques), il faudra décider d’une stratégie d’imputation au moment du nettoyage — c’est l’objet du tutoriel dédié au nettoyage. Pour l’instant, on prend simplement note.

Étape 4 — Statistiques descriptives

La méthode describe donne en une commande la moyenne, l’écart-type, le minimum, le maximum et les quartiles de toutes les colonnes numériques. Pour les colonnes catégorielles, describe(include="object") renvoie le nombre de valeurs uniques et la modalité la plus fréquente. Ces deux appels suffisent généralement à repérer les anomalies grossières : un montant maximum à 999 999 999 (probable valeur sentinelle), une moyenne d’âge à 145 ans, une catégorie qui domine à 99 %.

print(df.describe())                          # numériques
print(df.describe(include="object"))          # catégorielles

# Nombre de valeurs uniques par colonne
print(df.nunique().sort_values(ascending=False))

Le tableau describe doit être lu avec attention. Comparer la moyenne à la médiane (50 %) renseigne sur la dissymétrie : si la moyenne est très supérieure à la médiane, la distribution est étirée vers le haut (présence de gros montants exceptionnels par exemple). Comparer le minimum et le 25e percentile peut révéler des valeurs négatives suspectes dans une colonne qui ne devrait jamais l’être (un montant, un âge). C’est exactement le genre de signal faible qu’une analyse exploratoire bien menée doit attraper.

Étape 5 — Premier histogramme

Les statistiques numériques cachent la forme des distributions. Un histogramme la révèle immédiatement. matplotlib propose la fonction hist directement sur les Series pandas, ce qui rend le tracé extrêmement concis. On commence par la colonne montant, qui est le candidat le plus naturel pour comprendre le comportement métier.

fig, ax = plt.subplots(figsize=(8, 4))
df["montant"].hist(bins=50, ax=ax)
ax.set_title("Distribution des montants de commande")
ax.set_xlabel("Montant")
ax.set_ylabel("Nombre de commandes")
plt.tight_layout()
plt.savefig("histogramme_montant.png", dpi=120)
plt.show()

Le graphique apparaît dans le notebook. Si la distribution est très asymétrique — quelques très gros montants écrasent visuellement les autres barres — passer à une échelle logarithmique avec ax.set_yscale("log") permet de garder une lecture fine. Le réflexe à acquérir est de toujours sauvegarder une version PNG en parallèle de l’affichage : c’est ce fichier qu’on glissera dans un rapport, sans avoir à régénérer la figure plus tard. plt.tight_layout() garantit que les titres et étiquettes ne soient pas tronqués.

Étape 6 — Compter les modalités d’une variable catégorielle

Pour les variables catégorielles (statut de paiement, type de produit, ville), la méthode value_counts compte les occurrences de chaque modalité. Elle accepte un argument normalize=True qui renvoie les pourcentages plutôt que les comptes bruts. Combinée à un graphique en barres, elle donne une vue panoramique en deux lignes.

repartition = df["statut"].value_counts(normalize=True) * 100
print(repartition)

fig, ax = plt.subplots(figsize=(8, 4))
repartition.plot(kind="bar", ax=ax)
ax.set_title("Répartition des statuts de paiement (%)")
ax.set_ylabel("Pourcentage")
plt.xticks(rotation=45, ha="right")
plt.tight_layout()
plt.savefig("repartition_statut.png", dpi=120)
plt.show()

La sortie révèle souvent un déséquilibre fort : le statut « payé » domine, les statuts « annulé » ou « impayé » restent minoritaires. Cette asymétrie sera capitale plus tard si l’on cherche à prédire le risque d’impayé — c’est typiquement le scénario de classification déséquilibrée qui demande des techniques particulières. Pour l’instant, on note simplement la répartition.

Étape 7 — Convertir les dates et explorer les motifs temporels

Les colonnes de date arrivent presque toujours sous forme de chaînes de caractères et doivent être converties pour permettre les opérations temporelles (extraire le jour de la semaine, regrouper par mois, calculer une saisonnalité). La fonction pd.to_datetime est très tolérante sur les formats d’entrée mais on a intérêt à passer le format explicite quand on le connaît : c’est dix à vingt fois plus rapide sur de gros volumes et cela évite les ambiguïtés jour/mois sur les dates américaines vs européennes.

df["date"] = pd.to_datetime(df["date"], format="%Y-%m-%d", errors="coerce")
df = df.dropna(subset=["date"])    # retirer les lignes à date invalide

# Volume mensuel
volume_mensuel = df.set_index("date").resample("ME").size()

fig, ax = plt.subplots(figsize=(10, 4))
volume_mensuel.plot(ax=ax)
ax.set_title("Volume de commandes par mois")
ax.set_ylabel("Nombre de commandes")
plt.tight_layout()
plt.savefig("volume_mensuel.png", dpi=120)
plt.show()

Le graphique fait apparaître les pics et creux saisonniers. Les ruptures de pente, les paliers, les chutes brutales sont autant de signaux qui méritent une investigation : est-ce un effet calendaire (jours fériés, soldes), un changement de système informatique en interne, ou une vraie tendance métier ? resample("ME") agrège par fin de mois ; resample("W") donnerait un découpage hebdomadaire. L’option errors="coerce" de to_datetime remplace les chaînes non parsables par NaT au lieu de planter — utile quand on ne maîtrise pas la qualité d’entrée.

Étape 8 — Croiser deux variables

L’analyse univariée (une seule colonne à la fois) ne suffit jamais. Les questions intéressantes sont presque toujours croisées : le panier moyen varie-t-il selon le canal de paiement ?, le taux d’annulation est-il plus élevé certains jours de la semaine ?. Le croisement se fait avec groupby ou avec crosstab selon le type de question.

# Panier moyen par canal de paiement
panier_par_canal = df.groupby("canal_paiement")["montant"].agg(["mean", "median", "count"])
print(panier_par_canal)

# Taux d'annulation par jour de la semaine
df["jour_semaine"] = df["date"].dt.day_name()
taux = df.groupby("jour_semaine")["statut"].apply(lambda s: (s == "annule").mean() * 100)
ordre = ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"]
taux = taux.reindex(ordre)

fig, ax = plt.subplots(figsize=(8, 4))
taux.plot(kind="bar", ax=ax)
ax.set_title("Taux d'annulation par jour de la semaine (%)")
ax.set_ylabel("Pourcentage")
plt.tight_layout()
plt.savefig("taux_annulation_jour.png", dpi=120)
plt.show()

Le tableau du panier moyen révèle souvent des écarts importants entre canaux — le paiement en espèces sur livraison génère typiquement des paniers plus petits que le paiement carte, par exemple. Le graphique du taux d’annulation par jour met en évidence d’éventuels effets calendaires : un pic le lundi peut indiquer des commandes du week-end annulées au moment du traitement, ou refléter un comportement client spécifique qu’il faudra creuser.

Étape 9 — Heatmap de corrélation

Pour repérer rapidement les variables numériques liées entre elles, la matrice de corrélation reste l’outil de référence. Elle calcule le coefficient de Pearson entre chaque paire de colonnes numériques. Une valeur proche de 1 (ou -1) signale une forte dépendance linéaire, une valeur proche de 0 signale l’absence de relation linéaire — sans présager d’une éventuelle relation non linéaire qu’il faudra investiguer autrement.

import numpy as np

cols_num = df.select_dtypes(include=np.number).columns
corr = df[cols_num].corr()

fig, ax = plt.subplots(figsize=(8, 6))
im = ax.imshow(corr, cmap="coolwarm", vmin=-1, vmax=1)
ax.set_xticks(range(len(cols_num)))
ax.set_yticks(range(len(cols_num)))
ax.set_xticklabels(cols_num, rotation=45, ha="right")
ax.set_yticklabels(cols_num)
for i in range(len(cols_num)):
    for j in range(len(cols_num)):
        ax.text(j, i, f"{corr.iloc[i, j]:.2f}", ha="center", va="center", color="black", fontsize=8)
fig.colorbar(im, ax=ax)
ax.set_title("Matrice de corrélation des variables numériques")
plt.tight_layout()
plt.savefig("correlations.png", dpi=120)
plt.show()

La carte de chaleur attire l’œil sur les coins rouges (corrélation positive forte) et bleu foncé (corrélation négative forte). En analyse exploratoire, on cherche les paires inattendues : si nombre d’articles et montant sont corrélés à 0,95, c’est attendu et même rassurant. Si deux variables qu’on pensait indépendantes affichent une corrélation à 0,7, c’est un signal qu’il faut investiguer (variable proxy, fuite d’information, effet de confusion).

Étape 10 — Sauvegarder un rapport reproductible

Un notebook d’exploration n’a de valeur que s’il est rejouable. Avant de fermer la session, on s’assure que le notebook a bien été enregistré, on exporte une version HTML pour la partager sans demander à l’autre d’installer Python, et on commit le tout dans Git. Sans cette discipline, six mois plus tard, le travail est perdu.

jupyter nbconvert --to html exploration.ipynb
git add exploration.ipynb exploration.html *.png
git commit -m "EDA initiale du jeu transactions"

Le fichier HTML se partage par email ou se publie sur un dépôt interne. Le notebook reste consultable sur GitHub directement, mais le HTML garde l’avantage de figer le rendu si le notebook est modifié plus tard. Cette pratique de produire un livrable HTML systématique évite les questions du type « tu peux me renvoyer la version d’hier ? ».

Étape 11 — Détecter et qualifier les valeurs aberrantes

Une valeur aberrante n’est pas forcément une erreur de saisie. Un montant exceptionnellement haut peut correspondre à une commande B2B parfaitement légitime. C’est pourquoi on parle de détection plutôt que de nettoyage à ce stade : on identifie les points extrêmes, on les comprend, puis on décide. La règle de l’écart interquartile (IQR) reste la méthode la plus simple et la plus robuste pour ouvrir le bal.

q1 = df["montant"].quantile(0.25)
q3 = df["montant"].quantile(0.75)
iqr = q3 - q1
borne_basse = q1 - 1.5 * iqr
borne_haute = q3 + 1.5 * iqr

aberrants = df[(df["montant"] < borne_basse) | (df["montant"] > borne_haute)]
print(f"{len(aberrants)} lignes hors bornes [{borne_basse:.0f} ; {borne_haute:.0f}]")
print(aberrants["montant"].describe())

Le résultat indique le nombre de lignes hors bornes et leurs caractéristiques. Si elles représentent moins de 1 % du jeu de données et que leur examen confirme des cas légitimes (gros clients, commandes en lot), on les conserve mais on garde l’information pour la modélisation : un modèle linéaire sera très sensible à ces points, un modèle à arbres beaucoup moins. Si on identifie des erreurs claires (montants à 999 999 999 typiques d’une saisie sentinelle), on les filtre.

Étape 12 — Boîte à moustaches comparative

La boîte à moustaches (boxplot) est l’outil de référence pour comparer plusieurs distributions sur un même graphique. Elle synthétise médiane, quartiles, et points extrêmes en quelques traits. C’est particulièrement utile pour confirmer visuellement les écarts détectés au tableau croisé : si le panier moyen diffère significativement entre deux canaux, le boxplot le rend immédiatement lisible.

fig, ax = plt.subplots(figsize=(8, 5))
df.boxplot(column="montant", by="canal_paiement", ax=ax, showfliers=False)
ax.set_title("Distribution des montants par canal de paiement")
ax.set_ylabel("Montant")
plt.suptitle("")  # supprimer le titre auto-généré par pandas
plt.tight_layout()
plt.savefig("boxplot_montant_canal.png", dpi=120)
plt.show()

L’argument showfliers=False masque les points extrêmes pour rendre les boîtes lisibles — sans cela, sur des distributions asymétriques, les boîtes se retrouvent écrasées en bas du graphique. La ligne dans la boîte est la médiane, les bords sont les quartiles, et les moustaches s’étendent jusqu’à 1,5 fois l’IQR. Si deux canaux ont des boîtes qui ne se chevauchent pas, l’écart est probablement significatif statistiquement, ce qu’un test de Mann-Whitney pourra confirmer rigoureusement.

Erreurs fréquentes

ErreurCauseSolution
UnicodeDecodeError au chargementEncodage du fichier différent de UTF-8Spécifier encoding= »latin-1″ ou « cp1252 » dans read_csv
Colonnes fusionnées dans une seuleSéparateur mal détectéForcer sep= »; » ou sep= »\t » selon le cas
Dates incohérentes (day/month inversés)Format ambigu deviné par pandasToujours passer un format explicite à pd.to_datetime
Memory error sur gros fichierChargement complet en RAMLire par chunks avec chunksize ou passer à DuckDB/Polars
Histogramme illisible (queue très longue)Distribution asymétrique extrêmeset_yscale(« log ») ou filtrer les outliers avant tracé
Notebook qui ne se réexécute pasCellules dans le désordreRestart Kernel and Run All avant de partager
Graphique vide hors notebookplt.show() oublié en scriptToujours appeler plt.show() ou plt.savefig() explicitement

Tutoriels associés

Ressources officielles

FAQ

Faut-il préférer seaborn à matplotlib pour l’EDA ?

seaborn produit des graphiques statistiques plus jolis en moins de lignes mais s’appuie sur matplotlib en interne. Pour un débutant, matplotlib seul suffit largement et oblige à comprendre la structure des figures et axes — un investissement qui rentabilise toutes les bibliothèques de visualisation ultérieures.

Combien de lignes pandas peut-il gérer ?

Sur une machine avec 16 Go de RAM, pandas reste confortable jusqu’à environ 5 à 10 millions de lignes selon le nombre de colonnes. Au-delà, on bascule sur Polars, DuckDB, ou un traitement par chunks.

Combien de temps consacrer à l’EDA ?

Sur un projet sérieux, 30 à 50 % du temps total d’analyse passe en exploration et nettoyage. C’est normal et c’est rentable : un modèle entraîné sur des données mal comprises produit des résultats trompeurs.

Faut-il documenter son notebook d’exploration ?

Oui, en cellules Markdown qui décrivent ce qu’on cherche et ce qu’on observe. Trois mois plus tard, sans ces notes, on ne se souvient plus pourquoi telle transformation a été appliquée.

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité