24. NumPy

Arrays, shape, broadcasting, slicing avancé, algèbre linéaire, universal functions, random, performance.


24.1 Installation

pip install numpy
import numpy as np

24.2 Création d’arrays

# À partir d'une liste
a = np.array([1, 2, 3])              # 1D: shape (3,)
b = np.array([[1, 2], [3, 4]])       # 2D: shape (2, 2)
 
# Zéros, uns, identité
np.zeros((3, 4))                     # array de 0
np.ones((2, 3))                      # array de 1
np.eye(3)                            # matrice identité 3×3
 
# Plages
np.arange(10)                        # [0, 1, ..., 9]
np.arange(0, 1, 0.1)                # [0.0, 0.1, ..., 0.9]
np.linspace(0, 1, 5)                # [0.0, 0.25, 0.5, 0.75, 1.0]
 
# Aléatoire
np.random.rand(3, 4)                 # Uniforme [0, 1)
np.random.randn(100)                 # Normale centrée réduite
np.random.randint(0, 10, size=5)     # Entiers entre 0 et 10
np.random.seed(42)                   # Reproductibilité

24.3 Attributs fondamentaux

a = np.array([[1, 2, 3], [4, 5, 6]])
 
a.ndim          # 2 (nombre de dimensions)
a.shape         # (2, 3) (lignes, colonnes)
a.size          # 6 (nombre total d'éléments)
a.dtype         # dtype('int64')
a.itemsize      # 8 (octets par élément)
a.nbytes        # 48 (mémoire totale)

24.4 Indexation et slicing

a = np.array([[1, 2, 3, 4],
              [5, 6, 7, 8],
              [9, 10, 11, 12]])
 
a[0, 1]            # 2
a[1]               # [5, 6, 7, 8]
a[:, 1]            # [2, 6, 10] (colonne 1)
a[0:2, 1:3]        # [[2, 3], [6, 7]]
a[::2, ::2]        # [[1, 3], [9, 11]]
a > 5              # mask booléen
a[a > 5]           # [6, 7, 8, 9, 10, 11, 12]

Indexation avancée

indices = [0, 2]
a[:, indices]      # colonnes 0 et 2
 
lignes = [0, 1]
colonnes = [1, 2]
a[lignes, colonnes]  # [a[0,1], a[1,2]] = [2, 7]

24.5 Opérations vectorisées

a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
 
a + 10             # [11, 12, 13]
a * 2              # [2, 4, 6]
a + b              # [5, 7, 9]
a * b              # [4, 10, 18] (élément par élément)
a @ b              # 32 (produit scalaire)
np.dot(a, b)       # 32
 
np.sqrt(a)         # [1, 1.414, 1.732]
np.exp(a)          # [2.718, 7.389, 20.085]
np.sin(a)
np.sum(a)          # 6
np.mean(a)         # 2.0
np.std(a)          # écart-type
np.max(a)          # 3
np.argmax(a)       # 2 (index du max)
np.clip(a, 1, 2)   # [1, 2, 2]

24.6 Axes et agrégation

m = np.array([[1, 2, 3],
              [4, 5, 6]])
 
np.sum(m)           # 21 (tous les éléments)
np.sum(m, axis=0)   # [5, 7, 9]   somme par colonne
np.sum(m, axis=1)   # [6, 15]     somme par ligne
np.mean(m, axis=0)  # [2.5, 3.5, 4.5]
 
# Garder la dimension
np.sum(m, axis=1, keepdims=True)  # [[6], [15]]

24.7 Broadcasting

Opérations entre arrays de shapes différentes :

a = np.array([[1, 2, 3],
              [4, 5, 6]])       # shape (2, 3)
b = np.array([10, 20, 30])      # shape (3,)
 
a + b  # [[11, 22, 33],
       #  [14, 25, 36]]

Règles du broadcasting :

  1. Si les dimensions diffèrent, ajouter des 1 à gauche de la shape la plus petite
  2. Les dimensions sont compatibles si égales ou l’une vaut 1
  3. La dimension 1 est “étirée” pour correspondre
a = np.ones((3, 4))
b = np.ones((4,))
# broadcast: (3, 4) vs (1, 4) → (3, 4)
 
c = np.ones((3, 1))
d = np.ones((4,))
# broadcast: (3, 1) vs (1, 4) → (3, 4)

Exemple : normalisation par colonne

data = np.random.randn(100, 5)
moyenne = np.mean(data, axis=0)     # shape (5,)
écart = np.std(data, axis=0)        # shape (5,)
normalisé = (data - moyenne) / écart  # broadcasting

24.8 Changement de forme

a = np.arange(12)                   # [0...11]
 
a.reshape(3, 4)                     # 3×4
a.reshape(2, -1)                    # -1 = déduire la dimension
a.flatten()                         # copie 1D
a.ravel()                           # vue 1D (si possible)
a.T                                 # transposition
 
np.newaxis
a[:, np.newaxis]                    # (12,) → (12, 1)
a[np.newaxis, :]                    # (12,) → (1, 12)

24.9 Concaténation et split

a = np.array([[1, 2], [3, 4]])
b = np.array([[5, 6]])
 
np.concatenate((a, b), axis=0)      # [[1,2],[3,4],[5,6]]
np.vstack((a, b))                   # idem, vertical
np.hstack((a, b.T))                 # horizontal
np.stack((a, a), axis=2)            # nouvelle dimension
 
np.split(a, 2, axis=0)             # division en 2
np.array_split(a, 3, axis=0)       # division inégale

24.10 Algèbre linéaire

from numpy.linalg import inv, det, eig, svd, norm, qr
 
A = np.array([[1, 2], [3, 4]])
b = np.array([1, 2])
 
inv(A)             # inverse
det(A)             # déterminant
norm(A)            # norme Frobenius
norm(A, ord=2)     # norme spectrale
np.linalg.solve(A, b)  # résoudre Ax = b
np.linalg.lstsq(A, b)  # moindres carrés
 
eig(A)             # valeurs/vecteurs propres
svd(A)             # décomposition SVD
qr(A)              # décomposition QR
 
# Produit matriciel
A @ A
A.dot(A)
np.matmul(A, A)

24.11 Universal Functions (ufunc)

Opérations élément par élément optimisées en C :

np.add, np.subtract, np.multiply, np.divide
np.power, np.mod, np.abs
np.exp, np.log, np.log2, np.log10
np.sin, np.cos, np.tan
np.sinh, np.cosh
np.round, np.floor, np.ceil, np.trunc
np.greater, np.less, np.equal  # retournent des booléens

Fonctions personnalisées :

# Avec frompyfunc
def sigmoid(x):
    return 1 / (1 + np.exp(-x))
 
vec_sigmoid = np.frompyfunc(sigmoid, 1, 1)

24.12 Arrays masqués

data = np.array([1, -999, 2, -999, 3])
mask = data == -999
ma_data = np.ma.MaskedArray(data, mask=mask)
np.mean(ma_data)  # 2.0 (ignore les -999)

24.13 Sauvegarde et chargement

# Binaire NumPy
np.save("data.npy", a)
a = np.load("data.npy")
 
# Multiples arrays
np.savez("data.npz", a=a, b=b)
data = np.load("data.npz")
data["a"]
 
# Texte
np.savetxt("data.csv", a, delimiter=",")
a = np.loadtxt("data.csv", delimiter=",")

24.14 Performance

# LENT (boucle Python)
def lente(a, b):
    résultat = np.empty_like(a)
    for i in range(len(a)):
        résultat[i] = a[i] * b[i] + a[i] ** 2
    return résultat
 
# RAPIDE (vectorisé)
def rapide(a, b):
    return a * b + a ** 2

Règle d’or : éviter les boucles Python sur les arrays NumPy.

# Encore plus rapide : opérations sur place
a = np.random.rand(100_000)
b = np.random.rand(100_000)
np.multiply(a, b, out=a)  # a *= b (pas de copie)

24.15 einsum — notation Einstein

Notation compacte pour les opérations tensorielle :

A = np.array([[1, 2], [3, 4]])
B = np.array([[5, 6], [7, 8]])
 
# Produit matriciel
np.einsum("ij,jk->ik", A, B)      # = A @ B
 
# Trace
np.einsum("ii->", A)               # = trace(A)
 
# Produit élément par élément
np.einsum("ij,ij->ij", A, B)      # = A * B
 
# Somme sur un axe
np.einsum("ij->i", A)              # = sum(A, axis=1)

🔗 ← Retour au cours · ← précédent · Suivant →