📊 NumPy & Pandas · Débutant

NumPy & Pandas : manipulation de données

⏱ 40 minutes🐍 Python 3.11🔢 NumPy 1.26🐼 Pandas 2.2

NumPy et Pandas sont les fondations de la Data Science Python. NumPy apporte le calcul vectorisé sur des matrices, Pandas apporte les DataFrames pour manipuler des données tabulaires — comme Excel, mais avec du code.

Partie 1 — NumPy : calcul matriciel

📖 Terme : tableau NumPy (ndarray)

Définition : Un ndarray est un tableau multidimensionnel homogène de NumPy — tous les éléments ont le même type de données (int, float, etc.). Contrairement aux listes Python, les éléments sont stockés en mémoire de manière contiguë, ce qui permet des opérations très rapides.

But : Fournir une structure de données optimisée pour les calculs numériques et scientifiques, en remplaçant les boucles Python lentes par des opérations vectorisées compilées.

Pourquoi ici : NumPy est la base de tout traitement de données en Python. Comprendre le concept d'ndarray est essentiel avant de manipuler les données avec Pandas.

numpy_bases.py
import numpy as np

# ── Créer des tableaux ──
arr1d = np.array([1, 2, 3, 4, 5])
arr2d = np.array([[1, 2, 3], [4, 5, 6]])

print(arr2d.shape)    # (2, 3) — 2 lignes, 3 colonnes
print(arr2d.dtype)    # int64
print(arr2d.ndim)     # 2

# ── Tableaux spéciaux ──
zeros = np.zeros((3, 4))          # Matrice 3×4 de zéros
ones = np.ones((2, 3), dtype=np.float32)
eye = np.eye(4)                   # Matrice identité 4×4
rand = np.random.randn(100, 10)   # 100×10 — distribution normale
seq = np.arange(0, 10, 0.5)      # [0, 0.5, 1.0, ..., 9.5]

# ── Opérations vectorisées (SANS boucle !) ──
prix = np.array([100, 200, 150, 300, 250])
# Appliquer une TVA de 20% à tous les prix en une ligne
prix_ttc = prix * 1.20               # [120, 240, 180, 360, 300]
prix_reduits = np.where(prix > 200, prix * 0.9, prix)  # -10% si prix > 200

# ── Broadcasting ──
arr_3 = np.array([1, 2, 3])           # shape (3,)
arr_3x1 = np.array([[10], [20], [30]])  # shape (3, 1)
result = arr_3 + arr_3x1                  # Broadcasting — résultat shape (3, 3)
# [[11, 12, 13], [21, 22, 23], [31, 32, 33]]

# ── Statistiques ──
print(f"Moyenne : {prix.mean():.2f}")   # 200.00
print(f"Médiane : {np.median(prix):.2f}")
print(f"Écart-type : {prix.std():.2f}")
print(f"Min/Max : {prix.min()} / {prix.max()}")
print(f"Percentile 75% : {np.percentile(prix, 75)}")
Ce bloc montre la puissance de la vectorisation : au lieu de boucler sur chaque prix avec une boucle Python (qui serait 10-100x plus lente), on applique l'opération arithmétique directement au tableau entier. NumPy exécute l'opération sur tous les éléments en C compilé. Les opérations comme .mean(), .std() et .percentile() fonctionnent toutes sans boucle Python explicite, ce qui les rend extrêmement rapides même sur des millions d'éléments.
📖 Terme : Broadcasting

Définition : Mécanisme de NumPy qui permet de faire des opérations entre tableaux de formes différentes en "répétant" automatiquement les petits tableaux pour correspondre aux dimensions plus grandes.

But : Éviter de créer explicitement des copies ou de boucler manuellement pour aligner les formes.

Pourquoi ici : Le broadcasting est une source fréquente d'erreurs et de confusion — c'est un concept clé à maîtriser pour écrire du code NumPy efficace. Exemple concret : ajouter un vecteur de shape (3,) à une matrice de shape (3,1) donne une matrice (3,3). NumPy répète automatiquement le vecteur sur chaque ligne, puis le vecteur colonne sur chaque colonne.

Le broadcasting en action : arr_3 + arr_3x1. NumPy détecte que les shapes ne correspondent pas (3,) vs (3,1). Il applique les règles de broadcasting : aligner à droite, puis étendre les dimensions de taille 1. Le résultat est une matrice 3×3 où chaque ligne reçoit une copie du vecteur arr_3, mais avec chaque élément modifié par la valeur correspondante de arr_3x1 (10, 20 ou 30 selon la ligne).
📖 Terme : Vectorisation

Définition : Technique qui remplace les boucles Python explicites par des opérations sur des tableaux entiers. La vectorisation confie le calcul à du code optimisé (C/Fortran) au lieu de Python.

But : Accélérer drastiquement les calculs numériques — une opération vectorisée est typiquement 10 à 100 fois plus rapide qu'une boucle Python équivalente.

Pourquoi ici : Comprendre la vectorisation est central pour écrire du code Data Science performant. Au lieu de faire for i in range(len(arr)): result[i] = arr[i] * 2, on écrit simplement arr * 2. La différence de performance s'accroît avec la taille des données — sur 1 million d'éléments, c'est la différence entre quelques millisecondes et plusieurs secondes.

L'opération prix * 1.20 s'exécute entièrement en C compilé, sans passer par la boucle interpreter Python. NumPy traite tous les 5 prix simultanément (ou presque) en utilisant les instructions SIMD (Single Instruction Multiple Data) du CPU. C'est pourquoi NumPy est si rapide pour le calcul numérique.
numpy_matrices.py
import numpy as np

# ── Algèbre linéaire — cas pratique ML ──
# Régression linéaire : y = Xw (forme matricielle)

# Données : 5 maisons avec [surface, chambres, âge]
X = np.array([
    [50, 2, 10],
    [80, 3, 5],
    [120, 4, 2],
    [40, 1, 20],
    [100, 3, 8]
], dtype=np.float64)

y = np.array([200000, 320000, 480000, 150000, 380000])

# Normalisation (standardisation)
X_mean = X.mean(axis=0)  # Moyenne de chaque colonne
X_std = X.std(axis=0)
X_scaled = (X - X_mean) / X_std

# Ajouter colonne de biais (intercept)
X_b = np.column_stack([np.ones(len(X)), X_scaled])

# Équation normale : w = (X^T X)^-1 X^T y
w = np.linalg.lstsq(X_b, y, rcond=None)[0]
print(f"Paramètres : {w}")

# Prédiction pour une nouvelle maison [75m², 3ch, 7 ans]
new_house = np.array([75, 3, 7])
new_scaled = (new_house - X_mean) / X_std
prediction = np.dot([1, *new_scaled], w)
print(f"Prix prédit : {prediction:,.0f} €")

# ── Opérations matricielles ──
A = np.random.randn(4, 4)
print(f"Déterminant : {np.linalg.det(A):.4f}")
print(f"Rang : {np.linalg.matrix_rank(A)}")
eigenvalues, _ = np.linalg.eig(A)
print(f"Valeurs propres : {eigenvalues}")
Ce bloc montre comment utiliser NumPy pour résoudre un problème réel de Machine Learning : prédire le prix d'une maison à partir de ses caractéristiques (surface, chambres, âge). La normalisation des données (standardisation) est cruciale pour les algorithmes qui utilisent la distance ou les gradients — elle centre les données autour de 0 avec variance 1. L'équation normale utilise l'algèbre linéaire matricielle pour calculer les poids optimaux directement, sans itération. Pour prédire, on applique le produit scalaire (np.dot) entre les caractéristiques normalisées et les poids appris.

Partie 2 — Pandas : analyse de données réelles

📖 Terme : DataFrame

Définition : Un DataFrame est une table bidimensionnelle labellisée avec lignes et colonnes, similaire à une feuille Excel ou une table SQL. Les colonnes peuvent avoir des types différents (int, string, datetime, etc.), contrairement aux ndarrays.

But : Représenter et manipuler des données tabulaires réelles avec étiquettes intelligentes pour chaque axe (index pour les lignes, noms pour les colonnes).

Pourquoi ici : Les DataFrames sont la structure de données centrale en Pandas — 90% du travail de Data Science passe par les DataFrames. Un DataFrame combine la puissance du calcul NumPy avec l'usabilité des labels SQL.

📖 Terme : Series

Définition : Un Series est une colonne unique d'un DataFrame — un tableau unidimensionnel étiqueté avec un index. Elle combine un ndarray NumPy avec des étiquettes.

But : Représenter une seule variable ou dimension de données avec des étiquettes d'index, permettant l'accès par label ou position.

Pourquoi ici : Une Series est ce qu'on obtient quand on accède à une colonne d'un DataFrame (ex: df['name']). Comprendre les Series aide à comprendre les DataFrames — un DataFrame est en réalité un dictionnaire aligné de Series.

pandas_bases.py
import pandas as pd
import numpy as np

# ── Créer un DataFrame ──
data = {
    'name': ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve'],
    'age': [28, 34, 45, 29, 38],
    'salary': [45000, 62000, 85000, 51000, 72000],
    'department': ['Tech', 'Tech', 'Sales', 'HR', 'Tech'],
    'hire_date': pd.to_datetime(['2020-03-15', '2019-07-01', '2015-01-20', '2022-11-05', '2018-04-12'])
}
df = pd.DataFrame(data)

# ── Exploration ──
print(df.info())          # Types, valeurs nulles
print(df.describe())      # Statistiques descriptives des colonnes numériques
print(df.head(3))         # Premières lignes

# ── Sélection ──
tech_employees = df[df['department'] == 'Tech']
high_earners = df[df['salary'] > 60000][['name', 'salary']]

# ── Nouvelles colonnes calculées ──
df['years_employed'] = (pd.Timestamp.now() - df['hire_date']).dt.days / 365
df['seniority'] = pd.cut(
    df['years_employed'],
    bins=[0, 2, 5, 10, float('inf')],
    labels=['Junior', 'Mid', 'Senior', 'Lead']
)
Ce bloc crée un DataFrame à partir d'un dictionnaire Python et montre les opérations d'exploration courantes : .info() pour voir les types et les valeurs manquantes, .describe() pour des statistiques rapides (moyenne, écart-type, quartiles), et .head() pour visualiser les premières lignes. La sélection conditionnelle avec df[condition] filtre les lignes selon un critère booléen. Les colonnes calculées enrichissent le DataFrame avec de nouvelles variables dérivées (années d'emploi, catégories de séniorité).
pandas_avance.py
import pandas as pd
import numpy as np

# ── Simuler des données de ventes ──
np.random.seed(42)
dates = pd.date_range(start='2024-01-01', end='2024-12-31', freq='D')
sales_df = pd.DataFrame({
    'date': dates,
    'product': np.random.choice(['Laptop', 'Phone', 'Tablet'], len(dates)),
    'quantity': np.random.randint(1, 20, len(dates)),
    'unit_price': np.random.choice([999, 599, 449], len(dates)),
    'region': np.random.choice(['Nord', 'Sud', 'Est', 'Ouest'], len(dates))
})
sales_df['revenue'] = sales_df['quantity'] * sales_df['unit_price']
sales_df['month'] = sales_df['date'].dt.to_period('M')

# ── GroupBy — agrégations ──
monthly = sales_df.groupby('month').agg(
    total_revenue=('revenue', 'sum'),
    avg_daily_revenue=('revenue', 'mean'),
    transactions=('revenue', 'count')
).reset_index()
print(monthly.tail(3))
📖 Terme : GroupBy

Définition : Opération qui divise un DataFrame en groupes selon les valeurs d'une ou plusieurs colonnes, applique une fonction à chaque groupe, puis combine les résultats. Ce pattern s'appelle split-apply-combine.

But : Calculer des statistiques ou des transformations séparément pour chaque groupe de données (par catégorie, par période, par région, etc.).

Pourquoi ici : GroupBy est une des opérations les plus puissantes en Pandas. Au lieu de boucler manuellement sur chaque groupe, on décrit l'opération et Pandas l'applique automatiquement — c'est à la fois plus rapide et plus lisible. Dans l'exemple, on regroupe par mois, puis on calcule le revenu total, la moyenne et le nombre de transactions pour chaque mois.

Le GroupBy en action : sales_df.groupby('month').agg(...) divise d'abord les 365 lignes en 12 groupes (un par mois). Ensuite, pour chaque mois, Pandas calcule la somme des revenus, la moyenne des revenus, et le nombre de transactions. Enfin, .reset_index() ramène le 'month' en colonne ordinaire pour que le résultat soit un DataFrame simple avec une ligne par mois. C'est beaucoup plus efficace qu'une boucle Python manuelle.
Le pattern split-apply-combine est très puissant : on divise les données (split), on applique une agrégation ou transformation (apply), puis on combine le résultat. C'est beaucoup plus efficace que de boucler manuellement sur les groupes uniques et construire le résultat avec des listes Python — GroupBy est vectorisé et utilise NumPy/C sous le capot.
pandas_pivot.py
# ── Pivot table ──
pivot = pd.pivot_table(
    sales_df,
    values='revenue',
    index='region',
    columns='product',
    aggfunc='sum',
    fill_value=0
)
print(pivot)
📖 Terme : Pivot Table

Définition : Réorganisation de données tabulaires en réarrangeant les lignes et colonnes, avec agrégation des valeurs (sum, mean, count, etc.). Le résultat est une table où une dimension devient les lignes, une autre les colonnes, et la troisième est agrégée dans les cellules.

But : Résumer et croiser deux dimensions de données pour voir les patterns et les comparaisons rapidement — utile pour l'analyse croisée et les rapports.

Pourquoi ici : Pivot vs GroupBy : GroupBy est pour des résumés simples ou complexes (total par mois, avec multiples agrégations), pivot_table est pour croiser deux dimensions et voir le résultat dans une grille (revenu par région ET produit). Pivot_table appelle GroupBy en interne mais offre une interface plus intuitive pour ce cas d'usage spécifique.

La pivot_table restructure les données en mettant les lignes (régions) en index, les colonnes (produits) en entête, et les revenus comme valeurs. Le résultat est une grille où chaque cellule montre le revenu total pour une combinaison région-produit. C'est idéal pour voir rapidement : quel produit rapporte le plus par région ? Où manque-t-on de données ? fill_value=0 remplace les combinaisons manquantes par 0 (au lieu de NaN).
Utilise pivot_table quand tu dois croiser deux dimensions et voir les résultats dans une grille — idéal pour les rapports et les analyses croisées. Utilise groupby quand tu dois appliquer une transformation complexe, gérer plusieurs niveaux de groupage, ou garder la structure long format pour du machine learning.
pandas_nettoyage.py
# ── Nettoyage de données réelles ──
dirty_df = pd.DataFrame({
    'price': ['€1,299', 'N/A', '€899', '', '€1,499'],
    'date': ['15/01/2024', '2024-01-20', 'invalid', '22/01/2024', '23-01-2024']
})

# Nettoyer le prix
dirty_df['price_clean'] = (
    dirty_df['price']
    .str.replace('[€,]', '', regex=True)
    .replace(['N/A', ''], pd.NA)
    .astype('Float64')
)

# Analyser les valeurs manquantes
print(dirty_df.isnull().sum())                    # Compte par colonne
print(dirty_df.isnull().mean() * 100)              # % de valeurs manquantes

# Imputation — remplacer les NaN par la médiane
dirty_df['price_imputed'] = dirty_df['price_clean'].fillna(dirty_df['price_clean'].median())
Ce bloc montre le travail réel en Data Science : les données arrivent sales — avec des formats inconsistants (€1,299 vs numeric), des valeurs manquantes (N/A, chaînes vides), du bruit (dates en formats différents). Ici, on nettoie les prix en trois étapes : (1) supprimer les caractères non numériques avec regex, (2) remplacer les marqueurs de valeur manquante (N/A, chaîne vide) par pd.NA, (3) convertir en type Float64. .isnull().sum() compte les NaN par colonne pour diagnostiquer les problèmes. .fillna() impute les valeurs manquantes — ici avec la médiane, une stratégie robuste aux outliers. Ce type de nettoyage peut prendre 80% du temps en Data Science réel.
Pandas utilise NumPy sous le capot. La règle d'or : éviter les boucles Python sur les DataFrames — utilisez les méthodes vectorisées (.apply(), .str, .dt) qui sont 10 à 100x plus rapides. Par exemple, df['price'].str.replace(...) applique la transformation à toute la colonne en une seule opération optimisée, pas élément par élément.