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
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)
| Concept | Analogue | Description |
|---|---|---|
| Series | Colonne Excel | Tableau 1D avec index |
| DataFrame | Feuille Excel | Tableau 2D avec index+colonnes |
| Index | N° de ligne | Étiquettes des lignes (0, 1, 2… ou dates, noms…) |
| dtype | Format de cellule | Type de données d'une colonne (int64, float64, object…) |
| NaN | Cellule vide | Valeur 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
# 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
| notes | |
|---|---|
| Alice | 14 |
| Bob | 17 |
| Carl | 12 |
| Dana | 19 |
| Eve | 15 |
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
# 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
| nom | age | ville | salaire | |
|---|---|---|---|---|
| 0 | Alice | 28 | Paris | 3200 |
| 1 | Bob | 34 | Lyon | 4100 |
| 2 | Carl | 29 | Paris | 2900 |
| 3 | Dana | 41 | Lille | 5200 |
# 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
# ── 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")
# ── 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
# ── 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
# 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
# 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() — 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
# 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
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
df.info(), df.isnull().sum(), df.duplicated().sum()pd.to_datetime(), pd.to_numeric(errors="coerce")fillna() ou dropna() selon le contextedrop_duplicates(subset=...).str.strip().str.title()GroupBy & agrégations
# 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() — 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
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)
# 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
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 ...
# 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
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()
# 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.
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")
# 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.shape | Dimensions (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 |