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
| Erreur | Cause | Solution |
|---|---|---|
| UnicodeDecodeError au chargement | Encodage du fichier différent de UTF-8 | Spécifier encoding= »latin-1″ ou « cp1252 » dans read_csv |
| Colonnes fusionnées dans une seule | Séparateur mal détecté | Forcer sep= »; » ou sep= »\t » selon le cas |
| Dates incohérentes (day/month inversés) | Format ambigu deviné par pandas | Toujours passer un format explicite à pd.to_datetime |
| Memory error sur gros fichier | Chargement complet en RAM | Lire par chunks avec chunksize ou passer à DuckDB/Polars |
| Histogramme illisible (queue très longue) | Distribution asymétrique extrême | set_yscale(« log ») ou filtrer les outliers avant tracé |
| Notebook qui ne se réexécute pas | Cellules dans le désordre | Restart Kernel and Run All avant de partager |
| Graphique vide hors notebook | plt.show() oublié en script | Toujours appeler plt.show() ou plt.savefig() explicitement |
Tutoriels associés
- Nettoyer un jeu de données réel pas-à-pas
- Feature engineering avec scikit-learn
- 🔝 Retour au guide principal : Sciences de données : la stack pratique 2026
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.