Cours NumPy Complet 🔢

Les 12 chapitres essentiels — arrays, opérations vectorisées, algèbre linéaire et performance

12
Chapitres
100+
Exemples de code
NumPy 2+
Version
A1→C2
Niveaux

CHAPITRE 01

Introduction et installation

🔢 Premiers pas
# 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)

NumPy est la fondation du calcul scientifique en Python. Créé en 2005 par Travis Oliphant, il fournit le ndarray — un tableau multidimensionnel ultra-rapide. C’est la base de Pandas, Scikit-learn, TensorFlow, PyTorch et de tout l’écosystème data/ML Python.

CHAPITRE 02

Créer des arrays

📦 Fonctions de création
# À partir de listes
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)

CHAPITRE 03

Attributs et types

📋 Propriétés d’un array
m = np.array([[1, 2, 3], [4, 5, 6]])

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

🏷️ Types de données (dtype)
dtypeDescriptionTaille
int8 / int16 / int32 / int64Entier signé1 / 2 / 4 / 8 octets
uint8Entier non signé (0-255)1 octet (images)
float32 / float64Flottant4 / 8 octets
boolBooléen1 octet
complex64Nombre complexe8 octets
# Spécifier le type
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)

CHAPITRE 04

Indexation et slicing

🔍 Accéder aux éléments
# 1D
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.

CHAPITRE 05

Opérations vectorisées

⚡ Opérations élément par élément
a = np.array([1, 2, 3, 4])
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.

CHAPITRE 06

Statistiques et agrégation

📊 Fonctions d’agrégation
a = np.array([10, 25, 30, 15, 40])

# 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).

CHAPITRE 07

Broadcasting

📡 Règles du broadcasting
# Broadcasting = NumPy étend automatiquement les arrays
# 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.

CHAPITRE 08

Algèbre linéaire

📐 numpy.linalg
A = np.array([[1, 2], [3, 4]])
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)

CHAPITRE 09

Aléatoire

🎲 numpy.random (nouveau style)
# Générateur moderne (recommandé)
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.

CHAPITRE 10

Fichiers et I/O

💾 Sauvegarder et charger
# Format NumPy (.npy) — rapide, compact
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.

CHAPITRE 11

Performance

⚡ Optimiser NumPy
# 1. Vectoriser — JAMAIS de boucle for
# ❌ 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).

CHAPITRE 12

Bonnes pratiques

🧩 Fonctions utiles à connaître
# Empiler des arrays
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

✅ Bonnes pratiques

✅ À 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)

🧠 Quiz
Quelle est la différence entre * et @ pour les matrices ?
* = multiplication élément par élément (Hadamard). @ = produit matriciel (dot product). np.array([[1,2],[3,4]]) * np.array([[5,6],[7,8]]) → [[5,12],[21,32]]. Avec @ → [[19,22],[43,50]]. Pour le produit matriciel, toujours utiliser @.
Vue (view) vs copie (copy) — pourquoi c’est important ?
Un slice NumPy retourne une vue (pas de copie mémoire). Modifier la vue modifie l’original — c’est rapide mais source de bugs. b = a[0:5] est une vue. b = a[0:5].copy() est indépendant. Le fancy indexing a[[0,2,4]] retourne toujours une copie.

Cours NumPy Complet — Arrays, opérations vectorisées, algèbre linéaire et performance

Référence : numpy.org | JAX | Numba