Pandas : Manipuler et Analyser des Données en Python
Guide complet de Pandas pour charger, nettoyer, filtrer, agréger et exporter des données tabulaires en Python : Series, DataFrame, groupby, merge et dates.
InSkillCoach
Pandas : Manipuler et Analyser des Données en Python
Pandas est la bibliothèque de référence pour la manipulation de données tabulaires en Python. Construite sur NumPy, elle fournit deux structures de données essentielles, la Series et le DataFrame, ainsi qu’un arsenal complet pour charger des fichiers, nettoyer des valeurs manquantes, filtrer, agréger, joindre et exporter des données. Ce guide couvre le flux de travail complet d’une analyse : de la lecture d’un CSV jusqu’à l’export des résultats, en passant par les pièges classiques à éviter.
Series et DataFrame : les deux structures fondamentales
La Series : une colonne étiquetée
Une Series est un tableau unidimensionnel dont chaque valeur est associée à une étiquette d’index.
import pandas as pd
s = pd.Series([15, 12, 17, 9], index=["Alice", "Bob", "Chloé", "David"])
print(s)
# Alice 15
# Bob 12
# Chloé 17
# David 9
# dtype: int64
# Accès par étiquette ou par position
print(s["Chloé"]) # 17
print(s.iloc[0]) # 15
# Les opérations sont vectorisées, comme avec NumPy
print(s.mean()) # 13.25
print(s + 1) # chaque note augmentée de 1
Le DataFrame : un tableau à deux dimensions
Un DataFrame est un ensemble de colonnes (des Series) partageant le même index de lignes.
df = pd.DataFrame({
"nom": ["Alice", "Bob", "Chloé", "David"],
"age": [25, 32, 28, 41],
"ville": ["Paris", "Lyon", "Paris", "Nantes"],
"salaire": [42000, 38000, 51000, 46000],
})
print(df)
# nom age ville salaire
# 0 Alice 25 Paris 42000
# 1 Bob 32 Lyon 38000
# 2 Chloé 28 Paris 51000
# 3 David 41 Nantes 46000
# Une colonne est une Series
print(df["age"].dtype) # int64
print(df.shape) # (4, 4)
print(df.columns.tolist()) # ['nom', 'age', 'ville', 'salaire']
Lecture de fichiers avec read_csv
read_csv est la fonction la plus utilisée de Pandas. Ses options couvrent la quasi-totalité des formats CSV rencontrés en pratique.
# Lecture simple
df = pd.read_csv("ventes.csv")
# Options courantes
df = pd.read_csv(
"ventes.csv",
sep=";", # séparateur (point-virgule fréquent en France)
decimal=",", # virgule décimale
encoding="utf-8", # ou "latin-1" pour d'anciens fichiers
usecols=["date", "produit", "montant"], # ne charger que certaines colonnes
dtype={"produit": "string"}, # forcer un type
parse_dates=["date"], # convertir en datetime dès la lecture
na_values=["N/A", "inconnu"], # valeurs à traiter comme manquantes
nrows=1000, # limiter le nombre de lignes (utile pour explorer)
)
Pandas lit aussi de nombreux autres formats : read_excel, read_json, read_parquet, read_sql. Pour les gros volumes, le format Parquet est nettement plus rapide et plus compact que le CSV.
Inspection : head, info, describe
Les trois premières commandes à lancer sur tout nouveau jeu de données :
# Aperçu des premières lignes
print(df.head()) # 5 premières lignes (df.tail() pour les dernières)
# Structure : types, valeurs non nulles, mémoire
df.info()
# <class 'pandas.core.frame.DataFrame'>
# RangeIndex: 4 entries, 0 to 3
# Data columns (total 4 columns):
# # Column Non-Null Count Dtype
# --- ------ -------------- -----
# 0 nom 4 non-null object
# 1 age 4 non-null int64
# 2 ville 4 non-null object
# 3 salaire 4 non-null int64
# Statistiques descriptives des colonnes numériques
print(df.describe())
# age salaire
# count 4.000000 4.000000
# mean 31.500000 44250.000000
# std 6.952218 5560.275773
# min 25.000000 38000.000000
# max 41.000000 51000.000000
# Comptage des valeurs distinctes d'une colonne
print(df["ville"].value_counts())
# Paris 2
# Lyon 1
# Nantes 1
Sélection avec loc et iloc
Deux accesseurs complémentaires : loc sélectionne par étiquettes, iloc par positions entières.
# loc[lignes, colonnes] par étiquettes
print(df.loc[0, "nom"]) # Alice
print(df.loc[0:2, ["nom", "ville"]]) # lignes 0 à 2 INCLUSES, deux colonnes
# iloc[lignes, colonnes] par positions
print(df.iloc[0, 0]) # Alice
print(df.iloc[0:2, 0:2]) # lignes 0 et 1 (borne exclue), colonnes 0 et 1
# Toutes les lignes, une colonne
print(df.loc[:, "salaire"])
# Dernière ligne
print(df.iloc[-1])
Attention à la différence de convention : loc[0:2] inclut la borne 2 (logique d’étiquettes), alors que iloc[0:2] l’exclut (logique de positions, comme les listes Python).
Filtrage booléen
# Une condition
seniors = df[df["age"] > 30]
print(seniors)
# nom age ville salaire
# 1 Bob 32 Lyon 38000
# 3 David 41 Nantes 46000
# Conditions combinées : & (et), | (ou), ~ (non), parenthèses obligatoires
cible = df[(df["ville"] == "Paris") & (df["salaire"] >= 45000)]
print(cible)
# nom age ville salaire
# 2 Chloé 28 Paris 51000
# Appartenance à une liste
grandes_villes = df[df["ville"].isin(["Paris", "Lyon"])]
# Filtrage sur du texte
noms_a = df[df["nom"].str.startswith("A")]
# query : syntaxe alternative lisible
resultat = df.query("age > 30 and salaire < 47000")
Colonnes calculées
# Nouvelle colonne à partir des existantes
df["salaire_mensuel"] = df["salaire"] / 12
# Colonne conditionnelle avec numpy.where
import numpy as np
df["categorie"] = np.where(df["age"] < 30, "junior", "senior")
# Transformation par fonction (à réserver aux cas non vectorisables)
df["initiale"] = df["nom"].apply(lambda n: n[0])
# assign : créer des colonnes en chaînant les opérations
df2 = df.assign(
salaire_k=df["salaire"] / 1000,
age_en_2030=df["age"] + 4,
)
print(df2[["nom", "salaire_k", "age_en_2030"]].head(2))
# nom salaire_k age_en_2030
# 0 Alice 42.0 29
# 1 Bob 38.0 36
Privilégiez toujours les opérations vectorisées (df["a"] + df["b"], np.where, .str, .dt) : apply avec une lambda est souvent 10 à 100 fois plus lent.
Valeurs manquantes : isna, fillna, dropna
df_nan = pd.DataFrame({
"produit": ["A", "B", "C", "D"],
"prix": [10.0, None, 25.0, None],
"stock": [5, 3, None, 8],
})
# Détecter les valeurs manquantes
print(df_nan.isna())
print(df_nan.isna().sum()) # nombre de NaN par colonne
# produit 0
# prix 2
# stock 1
# Remplacer par une valeur fixe
df_rempli = df_nan.fillna({"prix": 0.0, "stock": 0})
# Remplacer par une statistique
df_nan["prix"] = df_nan["prix"].fillna(df_nan["prix"].mean())
print(df_nan["prix"])
# 0 10.0
# 1 17.5
# 2 25.0
# 3 17.5
# Supprimer les lignes contenant au moins un NaN
df_propre = df_nan.dropna()
# Supprimer seulement si une colonne précise est manquante
df_propre = df_nan.dropna(subset=["stock"])
Choisissez la stratégie selon le contexte : supprimer des lignes fait perdre de l’information, imputer une moyenne peut biaiser une analyse. Documentez toujours votre choix.
groupby et agrégations
Le pattern « split-apply-combine » : découper les données par groupe, appliquer un calcul, recombiner les résultats.
ventes = pd.DataFrame({
"region": ["Nord", "Sud", "Nord", "Sud", "Nord"],
"produit": ["A", "A", "B", "B", "A"],
"montant": [100, 150, 200, 120, 80],
})
# Agrégation simple
print(ventes.groupby("region")["montant"].sum())
# region
# Nord 380
# Sud 270
# Plusieurs statistiques à la fois
print(ventes.groupby("region")["montant"].agg(["sum", "mean", "count"]))
# sum mean count
# region
# Nord 380 126.666667 3
# Sud 270 135.000000 2
# Groupement sur plusieurs colonnes
print(ventes.groupby(["region", "produit"])["montant"].sum())
# region produit
# Nord A 180
# B 200
# Sud A 150
# B 120
# Agrégations nommées : colonnes de sortie explicites
resume = ventes.groupby("region").agg(
total=("montant", "sum"),
panier_moyen=("montant", "mean"),
)
print(resume)
# total panier_moyen
# region
# Nord 380 126.666667
# Sud 270 135.000000
Pour transformer un résultat groupé en tableau croisé, pivot_table est souvent plus direct :
tableau = ventes.pivot_table(index="region", columns="produit",
values="montant", aggfunc="sum", fill_value=0)
print(tableau)
# produit A B
# region
# Nord 180 200
# Sud 150 120
Combiner des tables : merge et concat
merge : les jointures façon SQL
clients = pd.DataFrame({
"client_id": [1, 2, 3],
"nom": ["Alice", "Bob", "Chloé"],
})
commandes = pd.DataFrame({
"client_id": [1, 1, 2, 4],
"montant": [50, 30, 70, 20],
})
# Jointure interne : uniquement les correspondances
interne = pd.merge(clients, commandes, on="client_id", how="inner")
print(interne)
# client_id nom montant
# 0 1 Alice 50
# 1 1 Alice 30
# 2 2 Bob 70
# Jointure gauche : tous les clients, même sans commande
gauche = pd.merge(clients, commandes, on="client_id", how="left")
# Chloé apparaît avec montant NaN
# how accepte aussi "right" et "outer"
concat : empiler des tables
janvier = pd.DataFrame({"produit": ["A", "B"], "ventes": [10, 20]})
fevrier = pd.DataFrame({"produit": ["A", "B"], "ventes": [15, 25]})
# Empilement vertical avec réindexation
annee = pd.concat([janvier, fevrier], ignore_index=True)
print(annee)
# produit ventes
# 0 A 10
# 1 B 20
# 2 A 15
# 3 B 25
Tris
# Tri sur une colonne
print(df.sort_values("age"))
# Tri décroissant sur plusieurs colonnes
print(df.sort_values(["ville", "salaire"], ascending=[True, False]))
# Tri sur l'index
print(df.sort_index())
# Les n plus grandes valeurs (plus rapide qu'un tri complet)
print(df.nlargest(2, "salaire")[["nom", "salaire"]])
# nom salaire
# 2 Chloé 51000
# 3 David 46000
Dates : to_datetime et resample
journal = pd.DataFrame({
"date": ["2026-01-05", "2026-01-12", "2026-02-03", "2026-02-20", "2026-03-01"],
"ca": [120, 80, 200, 150, 90],
})
# Conversion en datetime
journal["date"] = pd.to_datetime(journal["date"])
print(journal["date"].dtype) # datetime64[ns]
# L'accesseur .dt expose les composantes
journal["mois"] = journal["date"].dt.month
journal["jour_semaine"] = journal["date"].dt.day_name()
# resample : agrégation temporelle (nécessite un index datetime)
serie = journal.set_index("date")["ca"]
mensuel = serie.resample("ME").sum() # "ME" = fin de mois (pandas >= 2.2)
print(mensuel)
# date
# 2026-01-31 200
# 2026-02-28 350
# 2026-03-31 90
# Filtrer une plage de dates
recentes = journal[journal["date"] >= "2026-02-01"]
Si le format n’est pas standard, précisez-le : pd.to_datetime(s, format="%d/%m/%Y") évite les ambiguïtés jour/mois.
Export des résultats
# CSV (index=False pour ne pas écrire la colonne d'index)
df.to_csv("resultats.csv", index=False, sep=";", encoding="utf-8")
# Excel (nécessite openpyxl)
df.to_excel("resultats.xlsx", sheet_name="Analyse", index=False)
# Parquet : rapide, compact, types préservés (nécessite pyarrow)
df.to_parquet("resultats.parquet")
# JSON
df.to_json("resultats.json", orient="records", force_ascii=False)
Pièges courants : le SettingWithCopyWarning
Ce warning apparaît quand vous modifiez le résultat d’une sélection dont Pandas ne sait pas s’il s’agit d’une vue ou d’une copie.
# MAUVAIS : indexation chaînée, la modification peut être perdue
sous_ensemble = df[df["ville"] == "Paris"]
sous_ensemble["salaire"] = 0 # SettingWithCopyWarning
# BON : modifier le DataFrame original en une seule opération loc
df.loc[df["ville"] == "Paris", "salaire"] = 0
# BON : travailler sur une copie explicite et assumée
sous_ensemble = df[df["ville"] == "Paris"].copy()
sous_ensemble["salaire"] = 0 # aucun warning, df original intact
Autres pièges fréquents :
- Comparer avec
NaN:df["col"] == np.nanrenvoie toujoursFalse; utilisezdf["col"].isna(). - Types object silencieux : une colonne de nombres contenant une chaîne devient
object; vérifiez avecdf.dtypeset convertissez avecpd.to_numeric(s, errors="coerce"). inplace=True: déconseillé par l’équipe Pandas ; préférez la réaffectationdf = df.dropna().
Bonnes pratiques
- Inspectez systématiquement chaque nouveau jeu de données avec
head,infoetdescribeavant toute transformation. - Utilisez
loc/ilocexplicitement pour toute écriture, et bannissez l’indexation chaînée (df[...][...] = ...). - Vectorisez : évitez
applyet les boucles quand une opération sur colonne entière existe. - Parsez les dates dès la lecture avec
parse_dates, plutôt que de convertir après coup. - Nommez vos agrégations avec
agg(total=("montant", "sum"))pour obtenir des colonnes de sortie claires. - Préférez Parquet au CSV pour les fichiers intermédiaires : plus rapide, plus compact et sans perte de types.
- Enchaînez les transformations de façon lisible avec
assign,queryetpipeplutôt que de multiplier les variables intermédiaires.
Conclusion
Pandas couvre l’intégralité du cycle de vie d’une analyse de données : chargement robuste avec read_csv, exploration avec info et describe, nettoyage des valeurs manquantes, filtrage booléen, colonnes calculées, agrégations avec groupby, jointures avec merge, gestion fine des dates et export multi-formats. En comprenant la distinction entre loc et iloc et en évitant l’indexation chaînée, vous éliminez la grande majorité des bugs subtils. Les étapes suivantes naturelles sont la visualisation de vos DataFrames avec Matplotlib et l’analyse statistique avec SciPy, deux bibliothèques qui s’intègrent nativement avec Pandas.
À propos de InSkillCoach
Expert en formation et technologies
Coach spécialisé dans les technologies avancées et l'IA, porté par GNeurone Inc.
Certifications:
- AWS Certified Solutions Architect – Professional
- Certifications Google Cloud
- Microsoft Certified: DevOps Engineer Expert
- Certified Kubernetes Administrator (CKA)
- CompTIA Security+
Commentaires
Les commentaires sont alimentés par GitHub Discussions
Connectez-vous avec GitHub pour participer à la discussion