← Retour aux tutoriels

Analyse de données avec Pandas en Python : guide intermédiaire

pandaspythonanalyse-de-donneesdataframenettoyage-des-donneesgroupbyjointuresseries-temporellesdata-scienceetl

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

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 :


3) Comprendre la structure : info, describe, types

Inspection rapide

df.shape
df.info()
df.describe(include="all")

Dtypes : pourquoi c’est crucial

Les types influencent :

Exemples d’optimisation :

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

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

  1. Supprimer (si faible proportion et non critique) :
df_drop = df.dropna(subset=["prix_unitaire"])
  1. 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)
  1. 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()

Contrôler l’intégrité avec validate

df_enrichi = df.merge(clients, on="client_id", how="left", validate="many_to_one")

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()

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


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 :


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 :


Récapitulatif des commandes clés

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.