Analyse de données avec Pandas en Python : guide intermédiaire
Ce tutoriel propose une approche intermédiaire de l’analyse de données avec Pandas en Python. L’objectif est de consolider les bases (chargement, sélection, nettoyage) et d’aller plus loin avec des techniques très utilisées en pratique : types, valeurs manquantes, jointures, groupby avancé, fenêtrage temporel, tableaux croisés, chaînes, catégories, performance, et export.
1) Pré-requis et environnement
Installation
python -m pip install -U pandas numpy pyarrow openpyxl matplotlib seaborn
numpy: opérations numériques et tableaux.pyarrow: accélère certains traitements et permet Parquet.openpyxl: lecture/écriture Excel.matplotlib/seaborn: visualisation.
Import et options utiles
import pandas as pd
import numpy as np
pd.set_option("display.max_columns", 50)
pd.set_option("display.width", 120)
pd.set_option("display.float_format", lambda x: f"{x:,.3f}")
2) Créer un jeu de données réaliste (exemple reproductible)
Pour illustrer des cas concrets, on crée un petit jeu de données « ventes » avec des dates, des montants, des catégories, des valeurs manquantes et des incohérences.
import pandas as pd
import numpy as np
rng = np.random.default_rng(42)
n = 2000
df = pd.DataFrame({
"commande_id": np.arange(1, n+1),
"date_commande": pd.to_datetime("2024-01-01") + pd.to_timedelta(rng.integers(0, 180, size=n), unit="D"),
"client_id": rng.integers(1, 301, size=n),
"pays": rng.choice(["FR", "BE", "CH", "CA"], size=n, p=[0.65, 0.15, 0.10, 0.10]),
"canal": rng.choice(["web", "magasin", "marketplace"], size=n, p=[0.55, 0.35, 0.10]),
"categorie": rng.choice(["livres", "electronique", "maison", "mode"], size=n),
"quantite": rng.integers(1, 6, size=n),
"prix_unitaire": rng.normal(30, 12, size=n).round(2),
})
# Introduire des valeurs manquantes et anomalies
mask_na = rng.random(n) < 0.03
df.loc[mask_na, "prix_unitaire"] = np.nan
mask_neg = rng.random(n) < 0.01
df.loc[mask_neg, "quantite"] = -df.loc[mask_neg, "quantite"]
df["remise_pct"] = rng.choice([0, 5, 10, 15, np.nan], size=n, p=[0.55, 0.18, 0.12, 0.05, 0.10])
df["remise_pct"] = df["remise_pct"] / 100
df.head()
Points à observer :
prix_unitairecontient desNaN.quantitecontient des valeurs négatives (erreurs).remise_pctcontient desNaNet des remises en pourcentage.
3) Comprendre la structure : info, describe, types
Inspection rapide
df.shape
df.info()
df.describe(include="all")
info()révèle les types (dtypes) et le nombre de valeurs non-nulles.describe(include="all")inclut aussi les colonnes catégorielles (fréquences, top, etc.).
Dtypes : pourquoi c’est crucial
Les types influencent :
- la mémoire,
- la vitesse,
- les comparaisons (chaînes vs catégories),
- les opérations temporelles (datetime).
Exemples d’optimisation :
- passer
pays,canal,categorieencategory, - utiliser des entiers plus petits si possible.
df["pays"] = df["pays"].astype("category")
df["canal"] = df["canal"].astype("category")
df["categorie"] = df["categorie"].astype("category")
df.dtypes
4) Sélection, filtrage et indexation : éviter les pièges
Sélection de colonnes
df[["date_commande", "pays", "quantite"]].head()
Filtrage par conditions
df_fr_web = df[(df["pays"] == "FR") & (df["canal"] == "web")]
df_fr_web.head()
loc vs iloc
loc: sélection par labels (index, noms de colonnes), inclut les bornes sur les slices.iloc: sélection par positions (0..n-1), slice exclut la borne haute.
df.loc[0:3, ["commande_id", "pays", "quantite"]]
df.iloc[0:4, [0, 3, 6]]
Éviter SettingWithCopyWarning
Quand vous filtrez puis assignez, Pandas peut travailler sur une vue. Préférez .loc ou .copy().
df_tmp = df[df["pays"] == "FR"].copy()
df_tmp.loc[:, "flag_fr"] = True
5) Nettoyage : valeurs manquantes, doublons, incohérences
Valeurs manquantes : diagnostiquer
df.isna().mean().sort_values(ascending=False)
df.isna().sum()
Stratégies de traitement des NaN
- Supprimer (si faible proportion et non critique) :
df_drop = df.dropna(subset=["prix_unitaire"])
- Imputer (médiane par catégorie, par exemple) :
mediane_par_cat = df.groupby("categorie")["prix_unitaire"].transform("median")
df["prix_unitaire"] = df["prix_unitaire"].fillna(mediane_par_cat)
- Marquer (créer un indicateur) :
df["prix_manquant"] = df["prix_unitaire"].isna()
Dans notre cas, on a imputé prix_unitaire par médiane de categorie. C’est souvent plus robuste qu’une moyenne globale.
Corriger des incohérences : quantités négatives
Une quantité négative peut signifier un retour, ou une erreur de saisie. Il faut décider selon le contexte métier. Ici, on suppose une erreur et on prend la valeur absolue, tout en gardant une trace.
df["quantite_negative"] = df["quantite"] < 0
df.loc[df["quantite_negative"], "quantite"] = df.loc[df["quantite_negative"], "quantite"].abs()
Doublons
df.duplicated().sum()
df.duplicated(subset=["commande_id"]).sum()
Si commande_id doit être unique :
df = df.drop_duplicates(subset=["commande_id"], keep="first")
6) Créer des variables : assign, eval, calculs vectorisés
Montant brut, remise, montant net
On calcule des colonnes de façon vectorisée (rapide, idiomatique).
df["remise_pct"] = df["remise_pct"].fillna(0)
df = df.assign(
montant_brut = df["quantite"] * df["prix_unitaire"],
montant_remise = lambda x: x["montant_brut"] * x["remise_pct"],
montant_net = lambda x: x["montant_brut"] - x["montant_remise"],
)
df[["quantite","prix_unitaire","remise_pct","montant_net"]].head()
eval pour des expressions
Utile quand on enchaîne des calculs sur des noms de colonnes (attention à la lisibilité).
df.eval("montant_net2 = quantite * prix_unitaire * (1 - remise_pct)", inplace=True)
7) Tri, classement, recherche de valeurs extrêmes
Trier
df.sort_values(["date_commande", "montant_net"], ascending=[True, False]).head()
Top N par groupe (ex : top commandes par pays)
top_par_pays = (
df.sort_values("montant_net", ascending=False)
.groupby("pays", observed=True)
.head(5)
)
top_par_pays[["pays","commande_id","montant_net"]]
observed=True peut améliorer la performance avec des category en évitant des combinaisons inutiles.
8) Agrégations et groupby avancé
Agrégations simples
df.groupby("pays", observed=True)["montant_net"].sum().sort_values(ascending=False)
Agrégations multiples avec agg
resume = (
df.groupby(["pays", "canal"], observed=True)
.agg(
nb_commandes=("commande_id", "count"),
panier_moyen=("montant_net", "mean"),
ca_total=("montant_net", "sum"),
qte_totale=("quantite", "sum"),
)
.reset_index()
.sort_values("ca_total", ascending=False)
)
resume.head(10)
transform : ramener une statistique au niveau ligne
Exemple : part de la commande dans le CA de son pays.
ca_pays = df.groupby("pays", observed=True)["montant_net"].transform("sum")
df["part_ca_pays"] = df["montant_net"] / ca_pays
df[["pays","montant_net","part_ca_pays"]].head()
apply : puissant mais plus lent
apply permet des logiques complexes par groupe, mais peut être coûteux. À réserver aux cas où agg/transform ne suffisent pas.
Exemple : ratio entre le top 10% et le reste par pays :
def ratio_top_decile(g):
seuil = g["montant_net"].quantile(0.9)
top = g.loc[g["montant_net"] >= seuil, "montant_net"].mean()
reste = g.loc[g["montant_net"] < seuil, "montant_net"].mean()
return pd.Series({"moy_top10": top, "moy_reste90": reste, "ratio": top / reste})
ratios = df.groupby("pays", observed=True).apply(ratio_top_decile).reset_index()
ratios
9) Tableaux croisés et pourcentages
pivot_table
CA par catégorie et canal :
pivot = pd.pivot_table(
df,
index="categorie",
columns="canal",
values="montant_net",
aggfunc="sum",
fill_value=0,
observed=True
)
pivot
Ajouter des totaux
pivot["Total"] = pivot.sum(axis=1)
pivot.loc["Total"] = pivot.sum(axis=0)
pivot
Pourcentages par ligne
pivot_pct = pivot.div(pivot["Total"], axis=0).drop(index="Total", errors="ignore")
pivot_pct
10) Jointures (merge) : enrichir avec des dimensions
On crée une table « clients » et une table « pays » pour illustrer des enrichissements.
clients = pd.DataFrame({
"client_id": np.arange(1, 301),
"segment": rng.choice(["standard", "premium", "vip"], size=300, p=[0.75, 0.20, 0.05]),
"date_inscription": pd.to_datetime("2022-01-01") + pd.to_timedelta(rng.integers(0, 900, size=300), unit="D")
})
pays_dim = pd.DataFrame({
"pays": ["FR", "BE", "CH", "CA"],
"zone": ["UE", "UE", "Hors UE", "Hors UE"],
"devise": ["EUR", "EUR", "CHF", "CAD"]
})
merge (équivalent SQL JOIN)
df_enrichi = df.merge(clients, on="client_id", how="left")
df_enrichi = df_enrichi.merge(pays_dim, on="pays", how="left")
df_enrichi[["client_id","segment","pays","zone","devise"]].head()
how="left"conserve toutes les lignes dedf.- Vérifiez les clés : doublons côté dimension peuvent multiplier les lignes (effet « explosion »).
Contrôler l’intégrité avec validate
df_enrichi = df.merge(clients, on="client_id", how="left", validate="many_to_one")
many_to_one: plusieurs commandes pour un client, mais un seul enregistrement client parclient_id.
11) Dates et séries temporelles : resample, fenêtres, calendriers
Extraire des composantes de date
df["annee"] = df["date_commande"].dt.year
df["mois"] = df["date_commande"].dt.month
df["jour_semaine"] = df["date_commande"].dt.day_name()
Mettre la date en index et resample
CA hebdomadaire :
ts = (df.set_index("date_commande")["montant_net"]
.resample("W")
.sum()
)
ts.head()
CA mensuel par canal :
ts_canal = (
df.set_index("date_commande")
.groupby("canal", observed=True)["montant_net"]
.resample("MS")
.sum()
.reset_index()
)
ts_canal.head()
"W": semaine."MS": début de mois (Month Start), pratique pour l’alignement.
Moyennes mobiles (rolling)
Moyenne mobile 4 semaines du CA :
ts_mm4 = ts.rolling(window=4, min_periods=1).mean()
ts_mm4.head()
Comparaisons temporelles : variation MoM
Sur une série mensuelle :
ts_m = (df.set_index("date_commande")["montant_net"].resample("MS").sum())
variation_mom = ts_m.pct_change()
pd.DataFrame({"ca": ts_m, "variation_mom": variation_mom}).head(8)
12) Chaînes de caractères : nettoyage et normalisation
Supposons une colonne texte (ex : nom produit) avec des irrégularités. On simule une colonne produit.
df["produit"] = rng.choice(
[" Casque Audio", "casque audio", "CASQUE-AUDIO", "T-shirt", "t shirt", "Livre: Python ", "livre python"],
size=len(df)
)
Nettoyage avec .str
df["produit_net"] = (
df["produit"]
.str.strip()
.str.lower()
.str.replace(r"[-:]", " ", regex=True)
.str.replace(r"\s+", " ", regex=True)
)
df[["produit","produit_net"]].head(10)
Extraire des motifs
Exemple : détecter les livres :
df["est_livre"] = df["produit_net"].str.contains(r"\blivre\b", regex=True)
df["est_livre"].value_counts()
13) Catégories : ordre, regroupements, réduction mémoire
Les category sont utiles quand une colonne prend peu de valeurs distinctes.
Ordonner une catégorie (ex : segment)
df_enrichi = df_enrichi.merge(clients, on="client_id", how="left", validate="many_to_one")
ordre = pd.CategoricalDtype(categories=["standard", "premium", "vip"], ordered=True)
df_enrichi["segment"] = df_enrichi["segment"].astype(ordre)
df_enrichi["segment"].dtype
Groupby avec catégories ordonnées
df_enrichi.groupby("segment", observed=True)["montant_net"].mean()
14) Détection d’anomalies simples (approche pratique)
Z-score par catégorie (exemple)
On calcule un score par rapport à la distribution de la catégorie.
g = df.groupby("categorie", observed=True)["montant_net"]
m = g.transform("mean")
s = g.transform("std").replace(0, np.nan)
df["z_montant_cat"] = (df["montant_net"] - m) / s
df["anomalie"] = df["z_montant_cat"].abs() > 3
df["anomalie"].value_counts()
df.loc[df["anomalie"], ["commande_id","categorie","montant_net","z_montant_cat"]].head(10)
Cette méthode est simple et rapide, mais suppose une distribution à peu près stable ; en pratique, on la combine souvent avec des règles métier.
15) Visualisation rapide (diagnostic)
Même si Pandas n’est pas un outil de dataviz complet, des graphiques rapides aident beaucoup.
import matplotlib.pyplot as plt
ts_m = df.set_index("date_commande")["montant_net"].resample("MS").sum()
ax = ts_m.plot(kind="line", figsize=(10,4), title="CA mensuel")
ax.set_xlabel("Mois")
ax.set_ylabel("CA")
plt.tight_layout()
plt.show()
Répartition par canal :
df.groupby("canal", observed=True)["montant_net"].sum().plot(kind="bar", figsize=(6,4), title="CA par canal")
plt.tight_layout()
plt.show()
16) Performance : bonnes pratiques intermédiaires
Préférer le vectorisé à apply ligne par ligne
À éviter (lent) :
# Exemple pédagogique : à éviter sur gros volumes
# df["x"] = df.apply(lambda r: r["quantite"] * r["prix_unitaire"], axis=1)
À faire :
df["x"] = df["quantite"] * df["prix_unitaire"]
Utiliser category quand pertinent
Déjà fait pour pays, canal, categorie. Sur des millions de lignes, le gain mémoire peut être très important.
Réduire les colonnes inutiles tôt
colonnes_utiles = ["date_commande","client_id","pays","canal","categorie","quantite","prix_unitaire","remise_pct","montant_net"]
df_small = df[colonnes_utiles].copy()
Éviter les copies involontaires
- Utiliser
.locpour assigner. - Éviter les chaînes d’indexation du type
df[df["pays"]=="FR"]["montant_net"]=....
17) Export : CSV, Excel, Parquet
CSV
df.to_csv("ventes.csv", index=False, encoding="utf-8")
Excel (attention aux volumes)
df.head(5000).to_excel("ventes_extrait.xlsx", index=False)
Parquet (recommandé pour l’analytique)
df.to_parquet("ventes.parquet", index=False)
df2 = pd.read_parquet("ventes.parquet")
Parquet est souvent :
- plus compact,
- plus rapide à lire/écrire,
- mieux typé que CSV.
18) Mini-projet guidé : pipeline d’analyse réutilisable
On assemble une démarche typique : chargement → nettoyage → enrichissement → agrégations → export.
Étape A : fonction de nettoyage
def nettoyer_ventes(df):
df = df.copy()
# Types
for col in ["pays", "canal", "categorie"]:
df[col] = df[col].astype("category")
# Remise
df["remise_pct"] = df["remise_pct"].fillna(0)
# Quantités négatives
df["quantite_negative"] = df["quantite"] < 0
df.loc[df["quantite_negative"], "quantite"] = df.loc[df["quantite_negative"], "quantite"].abs()
# Imputation prix par médiane catégorie
med = df.groupby("categorie", observed=True)["prix_unitaire"].transform("median")
df["prix_unitaire"] = df["prix_unitaire"].fillna(med)
# Montants
df["montant_net"] = df["quantite"] * df["prix_unitaire"] * (1 - df["remise_pct"])
return df
Étape B : fonction de reporting
def rapport_ca(df):
# CA par mois et pays
ca_mois_pays = (
df.assign(mois=lambda x: x["date_commande"].dt.to_period("M").dt.to_timestamp())
.groupby(["mois", "pays"], observed=True)["montant_net"]
.sum()
.reset_index()
.sort_values(["mois", "montant_net"], ascending=[True, False])
)
# Top catégories global
top_categories = (
df.groupby("categorie", observed=True)["montant_net"]
.sum()
.sort_values(ascending=False)
.reset_index()
)
return ca_mois_pays, top_categories
Exécution
df_clean = nettoyer_ventes(df)
ca_mois_pays, top_categories = rapport_ca(df_clean)
ca_mois_pays.head()
top_categories
Export des résultats
ca_mois_pays.to_csv("ca_mois_pays.csv", index=False)
top_categories.to_csv("top_categories.csv", index=False)
19) Erreurs fréquentes et diagnostic
Confondre NaN et chaîne vide
Une chaîne vide "" n’est pas NaN. Pour traiter les deux :
col = "produit"
df[col] = df[col].replace("", np.nan)
Comparer des dates en texte
Si une date est en texte, convertir :
df["date_commande"] = pd.to_datetime(df["date_commande"], errors="coerce")
errors="coerce" transforme les valeurs invalides en NaT (équivalent NaN pour datetime), ce qui facilite le nettoyage.
Jointures qui dupliquent les lignes
Vérifier l’unicité côté dimension :
clients["client_id"].is_unique
clients["client_id"].duplicated().sum()
Si ce n’est pas unique, corriger avant le merge (déduplication, agrégation, règle métier).
20) Prochaines étapes
Pour aller au-delà de ce niveau intermédiaire :
- gérer des jeux de données volumineux (chunking CSV, Parquet partitionné),
- utiliser
pd.Grouperet des index multi-niveaux, - tester la qualité des données (contraintes, contrôles),
- intégrer Pandas dans un pipeline (scripts, notebooks, tâches planifiées),
- explorer des alternatives pour gros volumes (Dask, Polars) tout en gardant Pandas comme référence.
Récapitulatif des commandes clés
- Inspection :
df.info(),df.describe(),df.isna().sum() - Sélection :
loc,iloc, masques booléens - Nettoyage :
dropna,fillna,drop_duplicates - Création variables :
assign, opérations vectorisées,eval - Agrégation :
groupby().agg(),transform,apply - Restructuration :
pivot_table - Jointures :
merge(..., validate=...) - Temps :
.dt,set_index,resample,rolling,pct_change - Export :
to_csv,to_excel,to_parquet
Ce guide vous donne une base solide et réutilisable pour analyser des données avec Pandas dans des contextes proches du réel, en privilégiant des pratiques robustes, lisibles et performantes.