Python · Data Science

Pandas

Manipuler, nettoyer, analyser et visualiser des données tabulaires en Python — de la création d'un DataFrame jusqu'au projet d'analyse complet.

Introduction & installation

🐼

Pandas est la bibliothèque Python de référence pour la manipulation de données tabulaires. Elle s'appuie sur NumPy et fournit deux structures principales : la Series (colonne) et le DataFrame (tableau 2D). Elle est incontournable en data science, analyse financière et traitement de CSV/Excel.

Installation & imports
# Installation
pip install pandas openpyxl  # openpyxl pour lire/écrire xlsx

# Imports standard
import pandas as pd
import numpy  as np

# Vérifier la version
print(pd.__version__)  # ex: 2.2.1

# Options d'affichage utiles
pd.set_option("display.max_columns", 20)
pd.set_option("display.max_rows",    50)
pd.set_option("display.float_format", "{:.2f}".format)
ConceptAnalogueDescription
SeriesColonne ExcelTableau 1D avec index
DataFrameFeuille ExcelTableau 2D avec index+colonnes
IndexN° de ligneÉtiquettes des lignes (0, 1, 2… ou dates, noms…)
dtypeFormat de celluleType de données d'une colonne (int64, float64, object…)
NaNCellule videValeur manquante (Not a Number)

Pandas fonctionne en opérations vectorisées — appliquer une opération à toute une colonne d'un coup est bien plus rapide qu'une boucle Python. La règle d'or : éviter les boucles for sur les lignes d'un DataFrame.

Series

Créer et utiliser une Series
# Depuis une liste
notes = pd.Series([14, 17, 12, 19, 15],
                  name="notes")

# Avec index personnalisé
notes = pd.Series(
    [14, 17, 12, 19, 15],
    index=["Alice", "Bob", "Carl", "Dana", "Eve"],
    name="notes"
)

# Accès
notes["Alice"]    # → 14
notes[0]          # → 14 (par position)
notes[1:3]        # → Bob, Carl

# Opérations vectorisées
notes * 5          # sur 20 : multiplier par 5
notes + 2          # bonus de 2 points
notes[notes >= 15]  # filtrer les bonnes notes

# Statistiques de base
notes.mean()   # → 15.4
notes.max()    # → 19
notes.std()    # écart-type
notes.describe()  # count, mean, std, min, 25%…

# Valeurs uniques et compte
notes.unique()     # tableau des valeurs uniques
notes.value_counts()  # fréquences
notes.nunique()    # nombre de valeurs uniques
Series "notes"
notes
Alice14
Bob17
Carl12
Dana19
Eve15
Méthodes sur les chaînes (.str)
noms = pd.Series(["alice dupont", "bob martin", " carl"])

noms.str.upper()        # ALICE DUPONT…
noms.str.strip()        # retirer les espaces
noms.str.split()        # → liste de mots
noms.str.contains("bob") # → [False, True, False]
noms.str.replace("a", "@") # remplacement
noms.str.len()          # longueur de chaque chaîne
noms.str[:5]           # slicing

DataFrame

Créer un DataFrame
# Depuis un dictionnaire (le plus courant)
df = pd.DataFrame({
    "nom":   ["Alice", "Bob", "Carl", "Dana"],
    "age":   [28, 34, 29, 41],
    "ville": ["Paris", "Lyon", "Paris", "Lille"],
    "salaire": [3200, 4100, 2900, 5200],
})

# Depuis une liste de dicts
df = pd.DataFrame([
    {"nom": "Alice", "age": 28},
    {"nom": "Bob",   "age": 34},
])

# Explorer un DataFrame
df.shape       # (4, 4) — lignes × colonnes
df.dtypes      # types de chaque colonne
df.columns     # Index(['nom', 'age', ...])
df.index       # RangeIndex(start=0, stop=4)
df.info()      # résumé complet + mémoire
df.head(3)     # 3 premières lignes
df.tail(2)     # 2 dernières lignes
df.sample(2)   # 2 lignes aléatoires
df.describe()  # stats numériques
DataFrame "df"
nomagevillesalaire
0Alice28Paris3200
1Bob34Lyon4100
2Carl29Paris2900
3Dana41Lille5200
Ajouter et supprimer des colonnes
# Ajouter une colonne calculée
df["salaire_annuel"] = df["salaire"] * 12
df["senior"] = df["age"] >= 35

# assign() — version non-mutante (retourne nouveau df)
df2 = df.assign(
    bonus=lambda x: x["salaire"] * 0.1,
    nom_upper=lambda x: x["nom"].str.upper()
)

# Supprimer des colonnes
df.drop(columns=["salaire_annuel"], inplace=True)

# Renommer des colonnes
df.rename(columns={"nom": "name", "age": "âge"}, inplace=True)

# Changer le type d'une colonne
df["age"] = df["age"].astype("int32")
df["date"] = pd.to_datetime(df["date"])

Lecture & écriture — CSV, Excel, JSON

Lecture
# ── CSV ─────────────────────────────────────
df = pd.read_csv("data.csv")

# Options importantes
df = pd.read_csv(
    "data.csv",
    sep=";",             # séparateur (défaut: ",")
    encoding="utf-8",   # ou "latin-1" pour accents
    decimal=",",         # virgule décimale (FR)
    index_col=0,         # 1ère colonne = index
    parse_dates=["date"], # convertir en datetime
    usecols=["nom", "age"], # seulement ces colonnes
    nrows=1000,          # limiter le nombre de lignes
    na_values=["N/A", "--", ""]  # valeurs → NaN
)

# ── Excel ───────────────────────────────────
df = pd.read_excel("rapport.xlsx", sheet_name="Ventes")
df = pd.read_excel("rapport.xlsx", sheet_name=0)  # 1ère feuille

# Lire toutes les feuilles → dict
sheets = pd.read_excel("rapport.xlsx", sheet_name=None)

# ── JSON ────────────────────────────────────
df = pd.read_json("data.json")
df = pd.read_json("data.json", orient="records")

# Depuis une URL
df = pd.read_csv("https://exemple.com/data.csv")
Écriture
# ── CSV ─────────────────────────────────────
df.to_csv("sortie.csv", index=False)  # sans l'index
df.to_csv("sortie.csv", sep=";", decimal=",",
          encoding="utf-8-sig")  # utf-8-sig = BOM pour Excel FR

# ── Excel ───────────────────────────────────
df.to_excel("sortie.xlsx", index=False, sheet_name="Résultats")

# Plusieurs feuilles dans un même fichier
with pd.ExcelWriter("rapport.xlsx", engine="openpyxl") as writer:
    df_ventes.to_excel(writer, sheet_name="Ventes",  index=False)
    df_stock.to_excel(writer,  sheet_name="Stock",   index=False)
    df_clients.to_excel(writer,sheet_name="Clients", index=False)

# ── JSON ────────────────────────────────────
df.to_json("sortie.json", orient="records", indent=2,
           force_ascii=False)  # force_ascii=False = accents OK

# ── Autres formats ──────────────────────────
df.to_parquet("data.parquet")  # format colonne compressé (rapide)
df.to_sql("table", conn, if_exists="replace")  # SQLite/PostgreSQL
🇫🇷

Pour les fichiers CSV destinés à Excel en France : utiliser sep=";", decimal="," et encoding="utf-8-sig" — Excel FR reconnaît automatiquement ce format.

Sélection & filtrage

Sélection de colonnes et lignes
# ── Colonnes ────────────────────────────────
df["nom"]              # Series
df[["nom", "age"]]     # DataFrame avec 2 colonnes

# ── loc[] — par étiquettes ──────────────────
df.loc[0]              # ligne d'index 0
df.loc[0:2]            # lignes 0 à 2 (inclus !)
df.loc[0, "nom"]       # cellule (ligne 0, colonne "nom")
df.loc[0:3, "age":"ville"]  # tranche lignes × colonnes

# ── iloc[] — par position numérique ─────────
df.iloc[0]             # 1ère ligne
df.iloc[-1]            # dernière ligne
df.iloc[0:3, 1:3]      # lignes 0-2, colonnes 1-2
df.iloc[:, [0, 2]]    # toutes lignes, colonnes 0 et 2
Filtrage conditionnel
# Condition simple
df[df["age"] > 30]
df[df["ville"] == "Paris"]

# Conditions multiples (&, |, ~)
df[(df["age"] > 25) & (df["ville"] == "Paris")]
df[(df["age"] < 30) | (df["salaire"] > 4000)]
df[~(df["ville"] == "Lyon")]   # NOT

# isin — appartenance à une liste
df[df["ville"].isin(["Paris", "Lyon"])]

# between — plage de valeurs
df[df["age"].between(25, 35)]

# str.contains — filtre textuel
df[df["nom"].str.contains("ali", case=False)]

# query() — syntaxe lisible
df.query("age > 30 and ville == 'Paris'")
df.query("salaire between 3000 and 5000")

# Trier
df.sort_values("salaire", ascending=False)
df.sort_values(["ville", "age"])  # tri multi-colonnes

Statistiques descriptives

Statistiques sur colonnes
# Sur tout le DataFrame (colonnes numériques)
df.describe()
#        age      salaire
# count  4.00     4.000
# mean  33.00  3850.000
# std    5.89   989.949
# min   28.00  2900.000

# Sur une colonne
df["salaire"].mean()    # moyenne
df["salaire"].median()  # médiane
df["salaire"].std()     # écart-type
df["salaire"].var()     # variance
df["salaire"].sum()     # somme
df["salaire"].min()     # minimum
df["salaire"].max()     # maximum
df["salaire"].quantile(0.75)  # 3e quartile

# agg() — plusieurs stats en une fois
df["salaire"].agg(["mean", "std", "min", "max"])

# Sur les colonnes catégorielles
df["ville"].value_counts()         # fréquences
df["ville"].value_counts(normalize=True)  # proportions
df["ville"].nunique()              # nb de valeurs uniques
apply() et map()
# apply() — appliquer une fonction ligne par ligne
df["catégorie"] = df["salaire"].apply(
    lambda x: "haut" if x > 4000 else "standard"
)

# apply() sur plusieurs colonnes (axis=1)
df["profil"] = df.apply(
    lambda row: f"{row['nom']} ({row['ville']})",
    axis=1
)

# map() — remplacer des valeurs (Series)
mapping = {"Paris": "IDF", "Lyon": "ARA", "Lille": "HDF"}
df["région"] = df["ville"].map(mapping)

# np.where() — équivalent SI() d'Excel
df["bonus"] = np.where(
    df["salaire"] > 4000,
    df["salaire"] * 0.15,   # si vrai
    df["salaire"] * 0.05    # si faux
)

# pd.cut() — discrétiser en tranches
df["tranche_age"] = pd.cut(
    df["age"],
    bins=[0, 25, 35, 50, 100],
    labels=["junior", "confirmé", "senior", "expert"]
)

Nettoyage de données

Valeurs manquantes (NaN)
# Détecter
df.isnull()            # masque booléen
df.isnull().sum()      # nb de NaN par colonne
df.isnull().sum() / len(df) * 100  # % manquant

# Supprimer
df.dropna()            # lignes avec au moins 1 NaN
df.dropna(how="all")  # uniquement lignes 100% NaN
df.dropna(subset=["nom", "email"])  # NaN dans ces colonnes
df.dropna(thresh=3)   # garder si au moins 3 non-NaN

# Remplir
df.fillna(0)                     # tout remplacer par 0
df["age"].fillna(df["age"].mean())  # par la moyenne
df["ville"].fillna("Inconnu")    # par une chaîne
df.ffill()                        # forward fill (valeur précédente)
df.bfill()                        # backward fill

# Interpoler (séries temporelles)
df["temp"].interpolate(method="linear")
Doublons & types
# Doublons
df.duplicated()                    # masque
df.duplicated().sum()              # nb de doublons
df.drop_duplicates()               # supprimer tous les doublons
df.drop_duplicates(subset=["email"])  # sur une colonne
df.drop_duplicates(keep="last")   # garder le dernier

# Corriger les types
df["date"]    = pd.to_datetime(df["date"], dayfirst=True)
df["montant"] = pd.to_numeric(df["montant"], errors="coerce")
df["ville"]   = df["ville"].astype("category")  # économise la RAM

# Nettoyage de chaînes
df["nom"] = df["nom"].str.strip().str.title()
df["tel"] = df["tel"].str.replace(r"\D", "", regex=True)  # que les chiffres

# Remplacer des valeurs
df["sexe"].replace({"H": "Homme", "F": "Femme"}, inplace=True)
df["salaire"].clip(lower=0, upper=100000)  # écrêter les valeurs
1️⃣
Explorer : df.info(), df.isnull().sum(), df.duplicated().sum()
2️⃣
Corriger les types : pd.to_datetime(), pd.to_numeric(errors="coerce")
3️⃣
Traiter les NaN : fillna() ou dropna() selon le contexte
4️⃣
Supprimer les doublons : drop_duplicates(subset=...)
5️⃣
Normaliser les chaînes : .str.strip().str.title()

GroupBy & agrégations

groupby() — split → apply → combine
# Regrouper par ville, calculer la moyenne des salaires
df.groupby("ville")["salaire"].mean()
# ville
# Lille    5200.0
# Lyon     4100.0
# Paris    3050.0

# Plusieurs colonnes de regroupement
df.groupby(["ville", "tranche_age"])["salaire"].mean()

# Plusieurs agrégations simultanées
df.groupby("ville")["salaire"].agg(
    moyenne="mean",
    total="sum",
    effectif="count",
    max_sal="max"
)

# Agrégations différentes par colonne
df.groupby("ville").agg({
    "salaire": ["mean", "sum"],
    "age":     ["mean", "count"],
    "nom":     "nunique"
})

# Réinitialiser l'index après groupby
result = df.groupby("ville")["salaire"].mean().reset_index()
# → DataFrame avec colonnes "ville" et "salaire"
transform() et filter()
# transform() — renvoie une Series de même taille que df
# (utile pour ajouter une colonne calculée par groupe)
df["moy_ville"] = df.groupby("ville")["salaire"].transform("mean")
df["écart_moy"] = df["salaire"] - df["moy_ville"]

# Rang dans le groupe
df["rang_ville"] = df.groupby("ville")["salaire"] \
                     .transform(lambda x: x.rank(ascending=False))

# filter() — garder seulement certains groupes
# (conserver les villes avec plus de 1 employé)
df_filtre = df.groupby("ville").filter(lambda g: len(g) > 1)

# Pivot table (comme Excel)
pivot = df.pivot_table(
    values="salaire",
    index="ville",
    columns="tranche_age",
    aggfunc="mean",
    fill_value=0,
    margins=True   # ajouter ligne/colonne "Total"
)

Fusion & jointures

🔵🔵inner (défaut)
Seulement les lignes présentes dans les deux DataFrames. Élimine les non-correspondances.
⬜🔵left
Toutes les lignes du DataFrame gauche. NaN si pas de correspondance à droite.
🔵⬜right
Toutes les lignes du DataFrame droit. NaN si pas de correspondance à gauche.
⬜⬜outer
Toutes les lignes des deux DataFrames. NaN partout où il manque une correspondance.
merge() — jointure SQL-like
clients = pd.DataFrame({
    "id_client": [1, 2, 3],
    "nom": ["Alice", "Bob", "Carl"]
})
commandes = pd.DataFrame({
    "id_commande": [101, 102, 103, 104],
    "id_client":  [1, 2, 1, 9],   # 9 n'existe pas
    "montant":    [150, 80, 200, 50]
})

# INNER — seulement les clients qui ont commandé
pd.merge(clients, commandes, on="id_client")

# LEFT — tous les clients (même sans commande)
pd.merge(clients, commandes, on="id_client", how="left")

# Colonnes de jointure différentes
pd.merge(df1, df2,
         left_on="client_id", right_on="id")

# Jointure sur l'index
pd.merge(df1, df2, left_index=True, right_index=True)
concat() — empiler des DataFrames
# Empiler verticalement (ajouter des lignes)
df_total = pd.concat([df_jan, df_fev, df_mar],
                      ignore_index=True)  # réinitialiser l'index

# Empiler horizontalement (ajouter des colonnes)
df_wide = pd.concat([df_infos, df_scores], axis=1)

# Empiler plusieurs fichiers CSV d'un dossier
from pathlib import Path
dfs = [pd.read_csv(f) for f in Path("data/").glob("*.csv")]
df_all = pd.concat(dfs, ignore_index=True)

# join() — plus simple que merge pour les index
df1.join(df2, how="left", lsuffix="_x", rsuffix="_y")
🔍

Après un merge, toujours vérifier que le nombre de lignes est cohérent : un merge inner peut perdre des lignes, un outer peut en créer. Utiliser df.shape avant et après.

Reshape & pivot

melt() — wide → long
df_wide = pd.DataFrame({
    "ville": ["Paris", "Lyon"],
    "jan":   [120, 80],
    "fev":   [135, 95],
    "mar":   [140, 88],
})

df_long = df_wide.melt(
    id_vars="ville",       # colonnes à garder
    var_name="mois",       # nom de la nouvelle col clé
    value_name="ventes"   # nom de la nouvelle col valeur
)
#    ville  mois  ventes
# 0  Paris  jan      120
# 1   Lyon  jan       80
# 2  Paris  fev      135 ...
pivot() — long → wide
# L'inverse de melt()
df_wide2 = df_long.pivot(
    index="ville",    # colonne → index
    columns="mois",  # colonne → en-têtes
    values="ventes"  # valeurs à remplir
)

# Sérialisation des dates (utile après melt)
df["date"] = pd.to_datetime(df["mois"], format="%b")

# stack() / unstack() — manipulation de MultiIndex
df_stacked   = df.stack()   # colonnes → lignes
df_unstacked = df.unstack() # lignes → colonnes
📐

Règle mnémotechnique : melt() "fond" le tableau large en format long (une ligne par observation). pivot() "pivote" le format long en large. Le format long est préféré pour les visualisations et les groupby.

Visualisation

pandas.plot() — graphiques intégrés
import matplotlib.pyplot as plt

# Graphique en barres
df.groupby("ville")["salaire"].mean().plot(
    kind="bar", color="steelblue",
    title="Salaire moyen par ville",
    ylabel="Salaire (€)", rot=0)
plt.tight_layout()
plt.savefig("barres.png", dpi=150)
plt.show()

# Histogramme d'une distribution
df["salaire"].plot(kind="hist", bins=10,
                    edgecolor="white", color="#34d399")

# Courbe temporelle
df_ts.plot(kind="line", x="date", y="ventes",
           marker="o", figsize=(10, 4))

# Nuage de points (corrélation)
df.plot(kind="scatter", x="age", y="salaire",
        alpha=0.6, c="#f97316")

# Boîtes à moustaches
df.boxplot(column="salaire", by="ville")

# Matrice de corrélation
df["age", "salaire"].corr().style.background_gradient()
Salaire moyen par ville
Lille
Lyon
Paris
Plotly Express — graphiques interactifs
# pip install plotly
import plotly.express as px

# Barres interactives
fig = px.bar(df.groupby("ville")["salaire"]
               .mean().reset_index(),
             x="ville", y="salaire",
             title="Salaire moyen par ville",
             color="ville")
fig.show()

# Scatter plot coloré par catégorie
fig = px.scatter(df, x="age", y="salaire",
                 color="ville", hover_data=["nom"],
                 size="salaire")
fig.show()

# Exporter en HTML standalone
fig.write_html("graphique.html")

Projet — Analyse des ventes

📊

Projet complet : charger un CSV de ventes, le nettoyer, l'analyser par produit/région/mois, et exporter un rapport Excel avec plusieurs feuilles.

Pipeline complet
import pandas as pd
import matplotlib.pyplot as plt
from pathlib import Path

# ── 1. Charger ───────────────────────────────
df = pd.read_csv("ventes.csv", sep=";",
                 parse_dates=["date"], dayfirst=True,
                 decimal=",", encoding="utf-8-sig")
print(f"Chargé : {df.shape[0]} lignes, {df.shape[1]} colonnes")

# ── 2. Explorer ──────────────────────────────
print(df.dtypes)
print(df.isnull().sum())
print(df.describe())

# ── 3. Nettoyer ──────────────────────────────
df = df.drop_duplicates()
df["produit"] = df["produit"].str.strip().str.title()
df["région"]  = df["région"].str.strip()
df["montant"] = pd.to_numeric(df["montant"], errors="coerce")
df = df.dropna(subset=["montant", "date"])
df["montant"] = df["montant"].clip(lower=0)

# ── 4. Enrichir ──────────────────────────────
df["mois"]      = df["date"].dt.to_period("M")
df["trimestre"] = df["date"].dt.to_period("Q")
df["annee"]     = df["date"].dt.year

# ── 5. Analyser ──────────────────────────────
par_produit = df.groupby("produit").agg(
    total=("montant", "sum"),
    nb_ventes=("montant", "count"),
    panier_moyen=("montant", "mean")
).sort_values("total", ascending=False)

par_region = df.groupby("région")["montant"].sum() \
               .sort_values(ascending=False)

par_mois = df.groupby("mois")["montant"].sum()

top5 = par_produit.head(5)

# ── 6. Visualiser ────────────────────────────
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
top5["total"].plot(kind="barh", ax=axes[0],
                     title="Top 5 produits")
par_mois.plot(kind="line", ax=axes[1], marker="o",
              title="Évolution mensuelle")
plt.tight_layout()
plt.savefig("analyse.png", dpi=150)

# ── 7. Exporter ──────────────────────────────
with pd.ExcelWriter("rapport_ventes.xlsx", engine="openpyxl") as w:
    df.to_excel(w, sheet_name="Données brutes", index=False)
    par_produit.to_excel(w, sheet_name="Par produit")
    par_region.to_excel(w, sheet_name="Par région")
    par_mois.to_excel(w, sheet_name="Par mois")
print("✓ Rapport exporté dans rapport_ventes.xlsx")
Manipulation des dates
# Attributs utiles après parse_dates
df["date"].dt.year         # année
df["date"].dt.month        # mois (1-12)
df["date"].dt.day          # jour
df["date"].dt.day_name()   # "Monday", "Tuesday"…
df["date"].dt.dayofweek    # 0=lundi, 6=dimanche
df["date"].dt.quarter      # trimestre 1-4
df["date"].dt.to_period("M")  # "2024-01"
df["date"].dt.to_period("Q")  # "2024Q1"

# Filtrer par plage de dates
mask = (df["date"] >= "2024-01-01") & \
       (df["date"] <= "2024-06-30")
df_s1 = df[mask]

# Resampling (agréger par période)
df.set_index("date")["montant"] \
  .resample("ME").sum()  # ME = fin de mois
  # W = semaine, QE = fin de trimestre, YE = fin d'année
🏗️

Structurer l'analyse en fonctions dédiées (load_data(), clean_data(), analyze(), export()) facilite la réutilisation et les tests. Chaque fonction prend un DataFrame en entrée et retourne un DataFrame transformé.

Cheat Sheet Pandas

🏗️ Créer & explorer

pd.DataFrame(dict)Créer depuis un dict
pd.read_csv("f.csv")Lire un CSV
df.shapeDimensions (lignes, cols)
df.info()Types + valeurs manquantes
df.describe()Statistiques numériques
df.head(n)Premières n lignes

🔍 Sélection

df["col"]Une colonne → Series
df[["c1","c2"]]Plusieurs colonnes
df.loc[i, "col"]Par étiquette
df.iloc[0, 1]Par position
df[df["x"] > 5]Filtre booléen
df.query("x > 5")Filtre en chaîne

🧹 Nettoyage

df.isnull().sum()Compter les NaN
df.fillna(val)Remplir les NaN
df.dropna()Supprimer lignes NaN
df.drop_duplicates()Supprimer doublons
.str.strip().str.title()Nettoyer texte
pd.to_datetime()Convertir en date

📊 Analyse

df.groupby("c").agg()Agréger par groupe
pd.merge(df1, df2)Jointure SQL-like
pd.concat([df1,df2])Empiler des df
df.pivot_table()Tableau croisé
df.melt()Wide → Long
df.plot(kind=...)Graphique rapide