Pandas — Series et DataFrame#
Pandas est la bibliothèque incontournable de la manipulation de données tabulaires en Python. Son nom est un acronyme de Panel Data, un terme économétrique désignant des données qui combinent des dimensions temporelles et transversales. Depuis sa création par Wes McKinney en 2008, Pandas s’est imposé comme le standard pour charger, inspecter, nettoyer et transformer les données structurées dans des notebooks et des pipelines de production.
La force de Pandas réside dans son concept d”index : contrairement aux tableaux NumPy qui s’adressent uniquement par position entière, les structures Pandas associent à chaque ligne et à chaque colonne des étiquettes explicites. Ces étiquettes permettent l”alignement automatique des données lors des opérations arithmétiques ou des fusions de tables — une propriété qui élimine une catégorie entière de bugs difficiles à tracer dans les manipulations manuelles de tableaux.
Ce chapitre couvre les deux structures de données fondamentales de Pandas — Series et DataFrame — ainsi que les mécanismes d’indexation, de slicing, de lecture et d’écriture de fichiers, et les outils d’inspection initiale d’un jeu de données.
La Series#
Series
Une Series est un tableau unidimensionnel ordonné dont chaque élément est associé à une étiquette appelée index. Elle peut être vue comme un dictionnaire ordonné : les clés sont les étiquettes de l’index, et les valeurs sont les données. Tous les éléments d’une Series partagent le même type (dtype), et la Series peut contenir des valeurs manquantes (NaN ou pd.NA selon le type).
import pandas as pd
import numpy as np
# Création à partir d'une liste — index entier par défaut (0, 1, 2, …)
s1 = pd.Series([10, 20, 30, 40, 50])
print("Series avec index par défaut :")
print(s1)
print(f"\ndtype : {s1.dtype} | shape : {s1.shape}")
print()
# Création avec un index explicite
temperatures = pd.Series(
[12.5, 14.2, 18.7, 24.1, 28.3, 27.8, 22.4],
index=['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'],
name='Température (°C)'
)
print("Températures hebdomadaires :")
print(temperatures)
# Création à partir d'un dictionnaire
population = pd.Series({'Paris': 2161000, 'Lyon': 522000,
'Marseille': 862000, 'Toulouse': 479000})
print("\nPopulations :")
print(population)
Series avec index par défaut :
0 10
1 20
2 30
3 40
4 50
dtype: int64
dtype : int64 | shape : (5,)
Températures hebdomadaires :
Lun 12.5
Mar 14.2
Mer 18.7
Jeu 24.1
Ven 28.3
Sam 27.8
Dim 22.4
Name: Température (°C), dtype: float64
Populations :
Paris 2161000
Lyon 522000
Marseille 862000
Toulouse 479000
dtype: int64
Les opérations arithmétiques sur une Series sont vectorisées et alignées sur l’index. Si deux Series n’ont pas le même index, Pandas aligne les valeurs par étiquette et introduit des NaN pour les étiquettes manquantes.
# Alignement automatique sur l'index
s_a = pd.Series([1, 2, 3], index=['a', 'b', 'c'])
s_b = pd.Series([10, 20, 40], index=['a', 'b', 'd'])
print("s_a + s_b (alignement automatique) :")
print(s_a + s_b)
s_a + s_b (alignement automatique) :
a 11.0
b 22.0
c NaN
d NaN
dtype: float64
Le DataFrame#
DataFrame
Un DataFrame est une structure de données bidimensionnelle avec un index de lignes et un index de colonnes. Il peut être vu comme un dictionnaire ordonné de Series partageant le même index de lignes. Les colonnes peuvent avoir des types différents : une colonne peut être numérique, une autre catégorielle, une autre encore de type date. Conceptuellement, le DataFrame est l’équivalent Pandas d’une feuille de tableur ou d’une table de base de données relationnelle.
Création d’un DataFrame#
# Création à partir d'un dictionnaire de listes (le plus courant)
df = pd.DataFrame({
'Prénom': ['Alice', 'Bob', 'Carole', 'David', 'Émilie'],
'Âge': [28, 34, 29, 42, 31],
'Département': ['R&D', 'Marketing', 'R&D', 'Direction', 'Marketing'],
'Salaire': [48000, 55000, 51000, 72000, 53000],
'Actif': [True, True, False, True, True],
})
print(df)
print(f"\nShape : {df.shape} | dtypes :\n{df.dtypes}")
Prénom Âge Département Salaire Actif
0 Alice 28 R&D 48000 True
1 Bob 34 Marketing 55000 True
2 Carole 29 R&D 51000 False
3 David 42 Direction 72000 True
4 Émilie 31 Marketing 53000 True
Shape : (5, 5) | dtypes :
Prénom str
Âge int64
Département str
Salaire int64
Actif bool
dtype: object
Un DataFrame peut également être créé depuis d’autres sources :
# Depuis une liste de dictionnaires (une ligne = un dict)
lignes = [
{'ville': 'Paris', 'population': 2161000, 'superficie': 105},
{'ville': 'Lyon', 'population': 522000, 'superficie': 48},
]
df_villes = pd.DataFrame(lignes)
# Depuis un tableau NumPy
arr = np.random.default_rng(0).integers(0, 100, size=(4, 3))
df_arr = pd.DataFrame(arr, columns=['X', 'Y', 'Z'])
# Depuis une autre Series (colonne unique)
s = pd.Series([1, 2, 3], name='valeur')
df_from_series = s.to_frame()
Indexation : loc et iloc#
L’indexation est l’opération la plus fondamentale sur un DataFrame. Pandas propose deux mécanismes d’accès aux données qui couvrent des besoins complémentaires.
loc et iloc
loc effectue une indexation par étiquette : df.loc[étiquette_ligne, étiquette_colonne]. Les étiquettes sont inclusives aux deux extrémités dans les slices.
iloc effectue une indexation par position entière : df.iloc[i, j]. Les positions suivent la convention Python, exclusive à droite pour les slices (comme list[0:3]).
La distinction est fondamentale : si l’index du DataFrame est [2, 5, 7], alors df.loc[2] retourne la ligne étiquetée 2, tandis que df.iloc[0] retourne la première ligne quelle que soit son étiquette.
df2 = pd.DataFrame({
'nom': ['Alice', 'Bob', 'Carole', 'David'],
'score': [85, 92, 78, 88],
'grade': ['B', 'A', 'C', 'B'],
}, index=[10, 20, 30, 40]) # index non consécutif
print("DataFrame avec index non consécutif :")
print(df2)
print()
# loc : accès par étiquette
print("df2.loc[20] — ligne étiquetée 20 :")
print(df2.loc[20])
print()
print("df2.loc[10:30, 'nom':'score'] — slice par étiquettes :")
print(df2.loc[10:30, 'nom':'score'])
print()
# iloc : accès par position
print("df2.iloc[1] — deuxième ligne (position 1) :")
print(df2.iloc[1])
print()
print("df2.iloc[0:2, 0:2] — slice par positions :")
print(df2.iloc[0:2, 0:2])
DataFrame avec index non consécutif :
nom score grade
10 Alice 85 B
20 Bob 92 A
30 Carole 78 C
40 David 88 B
df2.loc[20] — ligne étiquetée 20 :
nom Bob
score 92
grade A
Name: 20, dtype: object
df2.loc[10:30, 'nom':'score'] — slice par étiquettes :
nom score
10 Alice 85
20 Bob 92
30 Carole 78
df2.iloc[1] — deuxième ligne (position 1) :
nom Bob
score 92
grade A
Name: 20, dtype: object
df2.iloc[0:2, 0:2] — slice par positions :
nom score
10 Alice 85
20 Bob 92
Sélection de colonnes#
La sélection d’une colonne par son nom avec df['colonne'] retourne une Series. La sélection de plusieurs colonnes avec df[['col1', 'col2']] retourne un DataFrame.
# Sélection d'une colonne → Series
scores = df2['score']
print(f"Type : {type(scores).__name__}")
print(scores)
print()
# Sélection de plusieurs colonnes → DataFrame
sous_df = df2[['nom', 'grade']]
print(sous_df)
Type : Series
10 85
20 92
30 78
40 88
Name: score, dtype: int64
nom grade
10 Alice B
20 Bob A
30 Carole C
40 David B
Sélection conditionnelle (masque booléen)#
La sélection par condition est l’une des opérations les plus puissantes de Pandas. Un masque booléen est une Series de valeurs True/False de même longueur que le DataFrame, obtenue par une comparaison.
# Masque booléen
masque_score = df2['score'] >= 85
print("Masque (score >= 85) :")
print(masque_score)
print()
# Application du masque
print("Lignes avec score >= 85 :")
print(df2[masque_score])
print()
# Combinaison de conditions
print("score >= 85 ET grade == 'B' :")
print(df2[(df2['score'] >= 85) & (df2['grade'] == 'B')])
Masque (score >= 85) :
10 True
20 True
30 False
40 True
Name: score, dtype: bool
Lignes avec score >= 85 :
nom score grade
10 Alice 85 B
20 Bob 92 A
40 David 88 B
score >= 85 ET grade == 'B' :
nom score grade
10 Alice 85 B
40 David 88 B
Note
Lors de la combinaison de conditions booléennes, il faut impérativement utiliser les opérateurs bit à bit & (ET), | (OU) et ~ (NON), et non les opérateurs logiques Python and, or, not. Ces derniers ne sont pas définis pour les Series et lèvent une exception. Chaque condition doit être entourée de parenthèses car la priorité de & est supérieure à celle de == en Python.
Slicing et accès rapide#
Au-delà de loc et iloc, Pandas offre des raccourcis d’accès utiles en pratique.
df.at[étiquette_ligne, étiquette_colonne] et df.iat[i, j] sont des équivalents scalaires de loc et iloc, optimisés pour l’accès à une seule valeur et environ deux fois plus rapides pour cet usage.
# Accès à une valeur scalaire
val = df2.at[20, 'score'] # étiquette → 92
val = df2.iat[1, 1] # position → 92
# Modifier une valeur
df2.at[20, 'grade'] = 'A+'
La méthode query() offre une syntaxe alternative plus lisible pour les sélections conditionnelles, en acceptant une chaîne de caractères décrivant la condition :
# Équivalent de df2[(df2['score'] >= 85) & (df2['grade'] == 'B')]
df2.query("score >= 85 and grade == 'B'")
# On peut référencer des variables Python avec @
seuil = 85
df2.query("score >= @seuil")
Types de données#
Chaque colonne d’un DataFrame a un type (dtype). La connaissance et la gestion des types est essentielle car ils impactent la mémoire utilisée, les opérations disponibles et les performances.
Types Pandas courants
Les types principaux sont :
int64,int32,int8: entiers signés (la largeur influe sur la mémoire et la plage de valeurs)float64,float32: nombres à virgule flottanteobject: type générique pour les chaînes de caractères ou les colonnes hétérogènesbool: booléendatetime64[ns]: horodatage nanosecondecategory: type catégoriel (valeurs prises dans un ensemble fini), économe en mémoireTypes étendus (depuis Pandas 1.0) :
Int64,Float64,boolean,string— versions nullables des types de base
df3 = pd.DataFrame({
'nom': pd.array(['Alice', 'Bob', 'Carole'], dtype='string'),
'âge': pd.array([28, 34, None], dtype='Int64'),
'note': [15.5, 12.0, 18.0],
'niveau': pd.Categorical(['junior', 'senior', 'junior'],
categories=['junior', 'confirmé', 'senior'],
ordered=True),
'date_rh': pd.to_datetime(['2022-03-15', '2020-07-01', '2023-01-10']),
})
print(df3)
print()
print(df3.dtypes)
print()
print(f"Mémoire : {df3.memory_usage(deep=True).sum() / 1024:.1f} Ko")
nom âge note niveau date_rh
0 Alice 28 15.5 junior 2022-03-15
1 Bob 34 12.0 senior 2020-07-01
2 Carole <NA> 18.0 junior 2023-01-10
nom string
âge Int64
note float64
niveau category
date_rh datetime64[us]
dtype: object
Mémoire : 0.4 Ko
La conversion de types s’effectue avec astype() :
df['age'] = df['age'].astype('Int32') # entier nullable
df['ville'] = df['ville'].astype('category') # économise la mémoire
df['date'] = pd.to_datetime(df['date_str']) # conversion en datetime
df['montant'] = pd.to_numeric(df['montant_str'], errors='coerce') # coerce → NaN si invalide
Lecture et écriture de fichiers#
L’une des forces de Pandas est la richesse de ses fonctions d’entrée-sortie. Elle couvre les formats les plus courants du monde de la data.
CSV#
Le format CSV (Comma-Separated Values) est le format d’échange le plus universel. pd.read_csv() est probablement la fonction Pandas la plus utilisée dans les projets réels.
# Lecture basique
df = pd.read_csv('donnees.csv')
# Avec options avancées
df = pd.read_csv(
'donnees.csv',
sep=';', # séparateur (parfois ';' ou '\t')
encoding='utf-8', # encodage (ou 'latin-1' pour certains fichiers Windows)
index_col='id', # colonne à utiliser comme index
usecols=['nom', 'age', 'score'], # lire seulement certaines colonnes
dtype={'age': 'Int32'}, # spécifier le type de certaines colonnes
parse_dates=['date_naissance'], # parser automatiquement les colonnes date
na_values=['N/A', 'MANQUANT', '-'], # valeurs à traiter comme NaN
nrows=1000, # lire seulement les 1000 premières lignes
)
# Sauvegarde
df.to_csv('resultat.csv', index=False, encoding='utf-8', sep=',')
Note
Le paramètre encoding est souvent source d’erreurs. Les fichiers produits par Excel en France sont souvent encodés en latin-1 ou cp1252 et non en utf-8. En cas d’erreur UnicodeDecodeError, essayer encoding='latin-1' ou encoding='cp1252'. La bibliothèque chardet (installable avec uv pip install chardet) peut détecter automatiquement l’encodage d’un fichier inconnu.
Excel#
# Lecture
df = pd.read_excel('rapport.xlsx', sheet_name='Données 2023',
header=1, # ligne d'en-tête (0-indexé)
skiprows=[2, 3]) # sauter des lignes spécifiques
# Lire toutes les feuilles en une seule fois
sheets = pd.read_excel('rapport.xlsx', sheet_name=None) # dict {nom: DataFrame}
# Sauvegarde
with pd.ExcelWriter('sortie.xlsx', engine='openpyxl') as writer:
df_ventes.to_excel(writer, sheet_name='Ventes', index=False)
df_charges.to_excel(writer, sheet_name='Charges', index=False)
JSON#
# Lecture depuis un fichier ou une URL
df = pd.read_json('data.json', orient='records')
# orient peut être : 'records', 'split', 'index', 'columns', 'values', 'table'
# Depuis une chaîne JSON
import json
json_str = '[{"nom": "Alice", "age": 28}, {"nom": "Bob", "age": 34}]'
df = pd.read_json(json_str)
# Sauvegarde
df.to_json('sortie.json', orient='records', force_ascii=False, indent=2)
Lecture d’une API REST JSON avec Pandas
Pandas peut lire directement des données issues d’une API HTTP via pd.read_json() ou en combinaison avec la bibliothèque requests :
import requests
import pandas as pd
# Appel à une API publique
response = requests.get('https://api.example.com/data?limit=100')
response.raise_for_status() # lève une exception si erreur HTTP
# Normaliser un JSON imbriqué en DataFrame plat
from pandas import json_normalize
data = response.json()
df = json_normalize(data['results'],
record_path='items',
meta=['id', 'date_creation'])
La fonction json_normalize() est particulièrement utile pour aplatir des structures JSON imbriquées en un DataFrame tabulaire.
Formats hautes performances#
Pour les volumes importants de données, les formats binaires offrent des performances de lecture et d’écriture bien supérieures au CSV.
# Parquet — format colonne, compression efficace, recommandé en production
df.to_parquet('data.parquet', compression='snappy', index=False)
df = pd.read_parquet('data.parquet', columns=['nom', 'date', 'montant'])
# Feather — lecture/écriture très rapide, idéal pour les échanges intermédiaires
df.to_feather('data.feather')
df = pd.read_feather('data.feather')
Inspection d’un nouveau jeu de données#
Lorsqu’on charge un jeu de données inconnu, un enchaînement systématique de commandes d’inspection permet de comprendre rapidement sa structure, son volume et la qualité des données.
| entreprise | ville | secteur | ca_m€ | effectif | fondation | cotée | |
|---|---|---|---|---|---|---|---|
| 0 | Société_0000 | Paris | nan | 5.48 | 3669 | 1992-01-01 | False |
| 1 | Société_0001 | Bordeaux | Santé | 0.96 | 2606 | 2021-01-01 | False |
| 2 | Société_0002 | Bordeaux | Tech | 5.83 | 1351 | 2006-01-01 | False |
| 3 | Société_0003 | Marseille | nan | 0.83 | 4001 | 2013-01-01 | False |
| 4 | Société_0004 | Marseille | Industrie | 4.95 | 1237 | 1999-01-01 | False |
# 1. Dimensions
print(f"Shape : {df_demo.shape[0]} lignes × {df_demo.shape[1]} colonnes")
print()
# 2. Aperçu des premières/dernières lignes
print("--- Premières lignes ---")
print(df_demo.head(3))
print()
# 3. Types et valeurs manquantes
print("--- info() ---")
df_demo.info()
Shape : 200 lignes × 7 colonnes
--- Premières lignes ---
entreprise ville secteur ca_m€ effectif fondation cotée
0 Société_0000 Paris nan 5.48 3669 1992-01-01 False
1 Société_0001 Bordeaux Santé 0.96 2606 2021-01-01 False
2 Société_0002 Bordeaux Tech 5.83 1351 2006-01-01 False
--- info() ---
<class 'pandas.DataFrame'>
RangeIndex: 200 entries, 0 to 199
Data columns (total 7 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 entreprise 200 non-null str
1 ville 200 non-null str
2 secteur 200 non-null str
3 ca_m€ 185 non-null float64
4 effectif 200 non-null int64
5 fondation 200 non-null datetime64[us]
6 cotée 200 non-null bool
dtypes: bool(1), datetime64[us](1), float64(1), int64(1), str(3)
memory usage: 14.8 KB
# 4. Statistiques descriptives — colonnes numériques
print("--- describe() — numériques ---")
print(df_demo.describe().round(2))
print()
# 5. Statistiques descriptives — colonnes de type object/catégoriel
print("--- describe() — object ---")
print(df_demo.describe(include='str'))
--- describe() — numériques ---
ca_m€ effectif fondation
count 185.00 200.00 200
mean 5.37 2569.02 2001-10-30 19:19:12
min 0.04 46.00 1980-01-01 00:00:00
25% 1.47 1515.00 1992-01-01 00:00:00
50% 3.52 2563.00 2002-01-01 00:00:00
75% 7.09 3770.00 2013-01-01 00:00:00
max 30.85 4991.00 2022-01-01 00:00:00
std 5.47 1409.47 NaN
--- describe() — object ---
entreprise ville secteur
count 200 200 200
unique 200 5 5
top Société_0000 Bordeaux Tech
freq 1 49 68
# 6. Valeurs manquantes
print("--- Valeurs manquantes par colonne ---")
print(df_demo.isna().sum().to_frame('nb_manquants').assign(
pct=lambda d: (d['nb_manquants'] / len(df_demo) * 100).round(1)
))
print()
# 7. Cardinalité des colonnes catégorielles
print("--- Cardinalité ---")
for col in df_demo.select_dtypes(include='str').columns:
print(f" {col:15s} : {df_demo[col].nunique()} valeurs uniques")
--- Valeurs manquantes par colonne ---
nb_manquants pct
entreprise 0 0.0
ville 0 0.0
secteur 0 0.0
ca_m€ 15 7.5
effectif 0 0.0
fondation 0 0.0
cotée 0 0.0
--- Cardinalité ---
entreprise : 200 valeurs uniques
ville : 5 valeurs uniques
secteur : 5 valeurs uniques
Note
La méthode info() est souvent plus informative que describe() comme premier regard sur un jeu de données : elle affiche le nombre de valeurs non nulles par colonne (détecter les colonnes creuses), le type de chaque colonne (détecter les types incorrects — par exemple une colonne de dates lue comme object) et la consommation mémoire totale. C’est la première commande à exécuter systématiquement après avoir chargé un fichier inconnu.
Résumé#
Ce chapitre a établi les fondements de la manipulation de données avec Pandas :
La
Seriesest un tableau unidimensionnel indexé : un vecteur de données avec des étiquettes. Elle supporte l’alignement automatique sur l’index lors des opérations arithmétiques.Le
DataFrameest la structure centrale de Pandas : un tableau bidimensionnel hétérogène avec un index de lignes et un index de colonnes, conceptuellement équivalent à une table de base de données.L”indexation par
loc(étiquettes) etiloc(positions entières) est la distinction fondamentale à maîtriser. La sélection conditionnelle par masque booléen ouquery()est l’opération de filtrage standard.La gestion des types (
dtype) est essentielle pour la mémoire, les performances et la correction des données.astype(),pd.to_datetime()etpd.to_numeric()sont les outils de conversion courants.Pandas lit et écrit les formats les plus courants : CSV (
read_csv/to_csv), Excel (read_excel/to_excel), JSON (read_json/to_json), et les formats haute performance Parquet et Feather pour les volumes importants.L”inspection initiale d’un jeu de données inconnu suit une séquence systématique :
shape,head(),info(),describe(), comptage des valeurs manquantes et cardinalité des colonnes catégorielles.