Cours NumPy Complet 🔢
Les 12 chapitres essentiels — arrays, opérations vectorisées, algèbre linéaire et performance
Introduction et installation
pip install numpy
# Import conventionnel
import numpy as np
# Créer un array
a = np.array([1, 2, 3, 4, 5])
print(a) # [1 2 3 4 5]
print(type(a)) # <class ‘numpy.ndarray’>
# Pourquoi NumPy est plus rapide que les listes Python ?
# Liste Python : chaque élément est un objet Python séparé
# NumPy array : bloc mémoire contigu de même type → C optimisé
# Exemple de vitesse
liste = list(range(1_000_000))
arr = np.arange(1_000_000)
# liste * 2 → boucle Python (lent)
# arr * 2 → opération C vectorisée (50-100x plus rapide)
Créer des arrays
a = np.array([1, 2, 3]) # 1D
m = np.array([[1, 2], [3, 4], [5, 6]]) # 2D (matrice 3×2)
# Zéros et uns
np.zeros(5) # [0. 0. 0. 0. 0.]
np.zeros((3, 4)) # Matrice 3×4 de zéros
np.ones((2, 3)) # Matrice 2×3 de uns
np.full((3, 3), 7) # Matrice 3×3 remplie de 7
# Identité
np.eye(3) # Matrice identité 3×3
# Séquences
np.arange(0, 10, 2) # [0 2 4 6 8] (start, stop, step)
np.linspace(0, 1, 5) # [0. 0.25 0.5 0.75 1. ] (5 points entre 0 et 1)
# Aléatoire
np.random.rand(3, 3) # Matrice 3×3 aléatoire [0, 1)
np.random.randint(0, 10, size=(2, 4)) # Entiers aléatoires 2×4
# Copie
b = a.copy() # Copie indépendante (pas une vue)
Attributs et types
m.shape # (2, 3) — 2 lignes, 3 colonnes
m.ndim # 2 — nombre de dimensions
m.size # 6 — nombre total d’éléments
m.dtype # int64 — type des éléments
m.nbytes # 48 — mémoire utilisée en octets
| dtype | Description | Taille |
|---|---|---|
| int8 / int16 / int32 / int64 | Entier signé | 1 / 2 / 4 / 8 octets |
| uint8 | Entier non signé (0-255) | 1 octet (images) |
| float32 / float64 | Flottant | 4 / 8 octets |
| bool | Booléen | 1 octet |
| complex64 | Nombre complexe | 8 octets |
a = np.array([1, 2, 3], dtype=np.float32)
# Convertir
a = a.astype(np.int8)
# Reshape — changer la forme sans copier
a = np.arange(12) # [0 1 2 … 11]
m = a.reshape(3, 4) # Matrice 3×4
m = a.reshape(3, –1) # -1 = calcul automatique → 3×4
v = m.flatten() # Retour en 1D (copie)
v = m.ravel() # Retour en 1D (vue, pas de copie)
Indexation et slicing
a = np.array([10, 20, 30, 40, 50])
a[0] # 10
a[–1] # 50
a[1:4] # [20 30 40]
a[::2] # [10 30 50] (un sur deux)
# 2D
m = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
m[0, 2] # 3 (ligne 0, colonne 2)
m[1] # [4 5 6] (ligne 1)
m[:, 0] # [1 4 7] (colonne 0)
m[0:2, 1:3] # [[2 3] [5 6]] (sous-matrice)
# Indexation booléenne (masque)
a = np.array([10, 20, 30, 40, 50])
mask = a > 25 # [False False True True True]
a[mask] # [30 40 50]
a[a > 25] # [30 40 50] (raccourci)
# Fancy indexing (index par array)
a[[0, 2, 4]] # [10 30 50]
# where — condition ternaire vectorisée
np.where(a > 25, a, 0) # [ 0 0 30 40 50] (si >25 → valeur, sinon 0)
Le slicing NumPy crée une vue, pas une copie. Modifier la vue modifie l’original : b = a[1:4]; b[0] = 999 → a est aussi modifié. Utilisez a[1:4].copy() pour une copie indépendante.
Opérations vectorisées
b = np.array([10, 20, 30, 40])
# Arithmétique (élément par élément)
a + b # [11 22 33 44]
a – b # [-9 -18 -27 -36]
a * b # [10 40 90 160]
a / b # [0.1 0.1 0.1 0.1]
a ** 2 # [1 4 9 16]
# Avec un scalaire
a * 3 # [3 6 9 12]
a + 10 # [11 12 13 14]
# Fonctions mathématiques
np.sqrt(a) # [1. 1.41 1.73 2. ]
np.exp(a) # [2.72 7.39 20.09 54.60]
np.log(a) # [0. 0.69 1.10 1.39]
np.sin(a) # Sinus
np.abs(a) # Valeur absolue
np.round(a, 2) # Arrondir
# Comparaisons (retournent des arrays booléens)
a > 2 # [False False True True]
a == 3 # [False False True False]
(a > 1) & (a < 4) # [False True True False]
Les opérations vectorisées sont 50-100x plus rapides que les boucles Python. NumPy exécute les calculs en C compilé sur des blocs mémoire contigus. Règle d’or : jamais de boucle for sur un array NumPy.
Statistiques et agrégation
# Statistiques de base
a.sum() # 120
a.mean() # 24.0
a.std() # 10.68 (écart-type)
a.var() # 114.0 (variance)
a.min() # 10
a.max() # 40
a.argmin() # 0 (index du min)
a.argmax() # 4 (index du max)
np.median(a) # 25.0
np.percentile(a, [25, 50, 75]) # Quartiles
a.cumsum() # [10 35 65 80 120] (somme cumulative)
# Sur les axes (2D)
m = np.array([[1, 2, 3],
[4, 5, 6]])
m.sum() # 21 (tout)
m.sum(axis=0) # [5 7 9] (somme par colonne)
m.sum(axis=1) # [6 15] (somme par ligne)
m.mean(axis=0) # [2.5 3.5 4.5] (moyenne par colonne)
axis=0 = colonnes (vertical ↓), axis=1 = lignes (horizontal →). Mnémotechnique : axis=0 « écrase » les lignes (résultat = 1 valeur par colonne), axis=1 « écrase » les colonnes (résultat = 1 valeur par ligne).
Broadcasting
# pour que les opérations soient possibles
# Scalaire + array
a = np.array([1, 2, 3])
a + 10 # [11 12 13] — 10 est « étendu » à [10 10 10]
# Vecteur colonne + vecteur ligne
col = np.array([[1], [2], [3]]) # Shape (3, 1)
row = np.array([10, 20, 30]) # Shape (3,)
col + row
# [[11 21 31]
# [12 22 32]
# [13 23 33]] — Shape (3, 3)
# Normaliser chaque colonne (moyenne=0, std=1)
data = np.random.randn(100, 4) # 100 lignes, 4 colonnes
normalized = (data – data.mean(axis=0)) / data.std(axis=0)
# data.mean(axis=0) → shape (4,) → broadcasté sur les 100 lignes
Règles du broadcasting : NumPy compare les dimensions de droite à gauche. Deux dimensions sont compatibles si elles sont égales ou si l’une vaut 1. La dimension de taille 1 est « étirée » pour correspondre à l’autre. Si les dimensions ne sont pas compatibles → erreur.
Algèbre linéaire
B = np.array([[5, 6], [7, 8]])
# Produit matriciel
A @ B # [[19 22] [43 50]]
np.dot(A, B) # Identique
# Transposée
A.T # [[1 3] [2 4]]
# Déterminant
np.linalg.det(A) # -2.0
# Inverse
np.linalg.inv(A) # [[-2. 1. ] [ 1.5 -0.5]]
# Valeurs propres
eigenvalues, eigenvectors = np.linalg.eig(A)
# Résoudre un système linéaire Ax = b
b = np.array([5, 11])
x = np.linalg.solve(A, b) # Solution
# Norme
v = np.array([3, 4])
np.linalg.norm(v) # 5.0 (norme euclidienne)
# Produit scalaire
np.dot(v, v) # 25 (pour les vecteurs 1D)
Aléatoire
rng = np.random.default_rng(seed=42) # Reproductible
# Uniformes
rng.random(5) # 5 flottants dans [0, 1)
rng.integers(0, 10, size=5) # 5 entiers dans [0, 10)
# Distributions
rng.normal(0, 1, size=1000) # Gaussienne (μ=0, σ=1)
rng.uniform(0, 100, size=50) # Uniforme [0, 100)
rng.poisson(5, size=100) # Poisson (λ=5)
rng.binomial(10, 0.5, size=100) # Binomiale (n=10, p=0.5)
# Mélanger et choisir
a = np.arange(10)
rng.shuffle(a) # Mélanger sur place
rng.choice(a, size=3) # 3 éléments aléatoires
rng.choice(a, size=3, replace=False) # Sans remise
Utilisez le nouveau style default_rng() au lieu de l’ancien np.random.rand(). Le nouveau générateur est plus rapide, statistiquement meilleur, et thread-safe. La seed garantit la reproductibilité des résultats.
Fichiers et I/O
np.save(‘array.npy’, a) # Sauvegarder un array
a = np.load(‘array.npy’) # Charger
# Plusieurs arrays (.npz)
np.savez(‘data.npz’, x=arr1, y=arr2)
data = np.load(‘data.npz’)
arr1 = data[‘x’]
# Compressé
np.savez_compressed(‘data.npz’, x=arr1, y=arr2)
# Texte (CSV-like)
np.savetxt(‘data.csv’, m, delimiter=‘,’, fmt=‘%.2f’)
m = np.loadtxt(‘data.csv’, delimiter=‘,’)
m = np.genfromtxt(‘data.csv’, delimiter=‘,’, skip_header=1)
.npy est 10-100x plus rapide que CSV pour charger/sauvegarder des arrays numériques. Utilisez .npy/.npz pour le stockage intermédiaire. CSV uniquement pour le partage avec d’autres outils. Pour les données tabulaires, préférez Parquet via Pandas.
Performance
# ❌ Lent
result = [] for x in arr:
result.append(x ** 2 + 2 * x + 1)
# ✅ Rapide (100x+)
result = arr ** 2 + 2 * arr + 1
# 2. Utiliser les bons dtypes
a = np.array(data, dtype=np.float32) # 2x moins de mémoire que float64
# 3. Vues au lieu de copies
view = a[:100] # Vue (pas de copie mémoire)
copy = a[:100].copy() # Copie (alloue de la mémoire)
# 4. Opérations in-place
a += 1 # In-place (pas de nouvel array)
a = a + 1 # Crée un nouvel array
# 5. np.einsum — opérations tensorielles optimisées
# Trace d’une matrice
np.einsum(‘ii’, m) # Somme diagonale
# Produit matriciel
np.einsum(‘ij,jk->ik’, A, B) # A @ B mais plus flexible
Pour encore plus de performance, considérez Numba (JIT compiler pour Python — décorez une fonction avec @numba.jit), CuPy (NumPy sur GPU — même API), ou JAX (NumPy + autograd + GPU/TPU par Google).
Bonnes pratiques
np.vstack([a, b]) # Vertical (lignes)
np.hstack([a, b]) # Horizontal (colonnes)
np.concatenate([a, b], axis=0)
# Trier
np.sort(a) # Copie triée
np.argsort(a) # Indices qui trieraient l’array
# Unique
np.unique(a) # Valeurs uniques
values, counts = np.unique(a, return_counts=True)
# Clip — borner les valeurs
np.clip(a, 0, 100) # Valeurs entre 0 et 100
# Meshgrid — grille de coordonnées
x = np.linspace(0, 1, 5)
y = np.linspace(0, 1, 5)
X, Y = np.meshgrid(x, y) # Grille 2D
✅ À FAIRE
• Vectoriser toutes les opérations
• default_rng(seed) pour le reproductible
• Spécifier le dtype (float32 si possible)
• .npy/.npz pour sauvegarder
• Broadcasting au lieu de boucles
• np.where pour les conditions
• axis=0 (colonnes), axis=1 (lignes)
• .copy() quand vous modifiez un slice
• Opérations in-place (+=) pour économiser la mémoire
❌ À ÉVITER
• Boucles for sur les arrays
• Ancien style np.random.rand()
• float64 quand float32 suffit
• Ignorer les vues vs copies (bugs subtils)
• CSV pour les gros arrays
• Créer des arrays dans une boucle (pré-allouer)
• np.matrix (déprécié, utiliser ndarray)
• Mélanger listes Python et arrays NumPy
• Ignorer le broadcasting (boucles inutiles)
