NumPy est le moteur invisible des calculs scientifiques en Python. Pourtant, 90% des utilisateurs l'utilisent mal. Voici comment gagner 50 fois en vitesse sans changer une ligne de code.

NUMPY, LE MOTEUR QUI FAIT TOURNER L'INTELLIGENCE ARTIFICIELLE

Derrière des bibliothèques comme Pandas, Scikit-Learn ou PyTorch, il y a un héros discret : NumPy. Ce module Python est le cœur battant des calculs numériques. Son secret ? Une implémentation ultra-optimisée en langage C, qui manipule directement des blocs de mémoire sans passer par les lenteurs de Python.

Mais voici le problème : la plupart des gens utilisent NumPy comme du Python normal, avec des boucles for et des calculs naïfs. Résultat ? Des programmes qui consomment des montagnes de mémoire et qui mettent des heures à s'exécuter. La bonne nouvelle, c'est qu'en changeant juste trois petites habitudes, tu peux accélérer tes calculs de 50 fois .

Avec NumPy mal utilisé, un calcul qui prend 11 secondes peut être réduit à 0,2 seconde. C'est 50 fois plus rapide, juste en changeant la façon d'écrire le code.

ASTUCE 1 : LA VECTORISATION, OU COMMENT TUER LES BOUCLES FOR

Le pire ennemi de la performance en calcul numérique, ce sont les boucles for. Chaque itération force Python à vérifier les types et à chercher les méthodes, ce qui ralentit tout. Pire encore : beaucoup croient que np.vectorize va les sauver. Mais c'est un piège . Cette fonction ne fait que cacher une boucle Python lente derrière une interface plus propre. Aucun gain de performance.

La solution ? La vectorisation. Au lieu d'écrire des boucles, tu utilises les fonctions natives de NumPy qui tournent directement en C. Par exemple, pour normaliser les colonnes d'une matrice (soustraire la moyenne et diviser par l'écart-type), voici comment faire mal :

import numpy as np
import time

# Création d'une matrice de 50 000 lignes et 1 000 colonnes
matrix = np.random.rand(50000, 1000)

start_time = time.time()

# Normalisation naïve avec des boucles for
res = matrix.copy()
for col in range(matrix.shape[1]):
    col_mean = np.mean(matrix[:, col])
    col_std = np.std(matrix[:, col])
    for row in range(matrix.shape[0]):
        res[row, col] = (matrix[row, col] - colmean) / colstd

durationloop = time.time() - starttime
print(f"Calcul avec boucles for terminé en : {duration_loop:.4f} secondes")
Ce code met 10,9986 secondes pour traiter la matrice. C'est plus lent qu'un escargot en montée .

Voici comment faire bien, avec la vectorisation :

import numpy as np
import time

# Création d'une matrice de 50 000 lignes et 1 000 colonnes
matrix = np.random.rand(50000, 1000)

start_time = time.time()

# Calcul des moyennes et écarts-types par colonne (en C compilé)
means = np.mean(matrix, axis=0)
stds = np.std(matrix, axis=0)

# Vectorisation : NumPy étire automatiquement les tableaux
res_vectorized = (matrix - means) / stds

durationvectorized = time.time() - starttime
print(f"Calcul vectorisé terminé en : {duration_vectorized:.4f} secondes")
Ce même calcul prend maintenant 0,1972 seconde. C'est 55 fois plus rapide .

Comment ça marche ? NumPy utilise la diffusion (broadcasting). Quand tu fais (matrix - means), NumPy aligne automatiquement les tableaux de tailles différentes. La matrice a une forme (50000, 1000) et means a une forme (1000,). NumPy étire mentalement le tableau means pour qu'il corresponde à la matrice, sans copier les données. Tout se passe en mémoire, et les calculs sont poussés vers le processeur via des instructions SIMD (Single Instruction, Multiple Data), comme si tu avais 1000 calculs en parallèle.

ASTUCE 2 : LES OPÉRATIONS IN-PLACE, OU COMMENT ÉCONOMISER LA MÉMOIRE

Quand tu écris y = 2 * x + 3, tu penses que NumPy va optimiser le calcul. Mais en réalité, il crée des tableaux temporaires à chaque étape : d'abord 2 * x, puis 2 * x + 3. Pour des tableaux de millions d'éléments, ça signifie des allocations de mémoire inutiles et un gaspillage de temps.

Prenons un exemple concret avec un tableau d'un million d'éléments :

import numpy as np
import time

# Création d'un tableau de 10 millions d'éléments
x = np.random.rand(10000000)
scale = 2.5
offset = 1.2

start_time = time.time()

# Calcul naïf : création de tableaux temporaires
# (scale  x) puis (scale  x + offset)
y_naive = scale * x + offset

durationnaive = time.time() - starttime
print(f"Calcul naïf terminé en : {duration_naive:.4f} secondes")

Ce code prend 0,0393 seconde. Maintenant, voici comment faire mieux en utilisant les opérations in-place :

import numpy as np
import time

# Création d'un tableau de 10 millions d'éléments
x = np.random.rand(10000000)
scale = 2.5
offset = 1.2

start_time = time.time()

# Pré-allocation du tableau final
yoptimized = np.emptylike(x)

# Calcul direct dans le tableau cible, sans tableaux temporaires
np.multiply(x, scale, out=y_optimized)
np.add(yoptimized, offset, out=yoptimized)

durationoptimized = time.time() - starttime

print(f"Calcul optimisé terminé en : {duration_optimized:.4f} secondes")
print(f"Accélération : {durationnaive / durationoptimized:.2f}x plus rapide .")
Ce code optimisé prend 0,0133 seconde. C'est presque 3 fois plus rapide que la version naïve, et surtout, il n'utilise pas de mémoire supplémentaire.

L'astuce ? On pré-alloue le tableau y_optimized une seule fois. Ensuite, on utilise les fonctions NumPy comme np.multiply et np.add avec le paramètre out pour écrire directement les résultats dans ce tableau. Résultat : plus de tableaux temporaires, moins de pression sur la mémoire vive, et des calculs plus rapides grâce à une meilleure utilisation du cache CPU.

ASTUCE 3 : LES VUES MÉMOIRE, OU COMMENT ÉVITER LES COPIES INUTILES

Un autre piège classique : les copies de données. Quand tu extrais une sous-matrice avec des indices avancés, NumPy crée une copie physique des données. Pour une matrice de 10 000 x 10 000, ça peut prendre 0,15 seconde, soit 15 000 fois plus lent qu'un simple accès .

Voici comment faire mal :

import numpy as np
import time

# Création d'une matrice de 10 000 x 10 000 éléments
matrix = np.random.rand(10000, 10000)

start_time = time.time()

# Extraction avec indices avancés : FORCE UNE COPIE
rows = np.arange(0, matrix.shape[0], 2)
cols = np.arange(0, matrix.shape[1], 2)
submatrixcopy = matrix[rows[:, None], cols]

durationcopy = time.time() - starttime
print(f"Extraction avec copie terminée en : {duration_copy:.4f} secondes")
Ce code met 0,1575 seconde pour extraire une sous-matrice. C'est comme si tu copiais un livre entier pour lire une seule page.

Voici comment faire bien, avec les vues mémoire :

import numpy as np
import time

# Création d'une matrice de 10 000 x 10 000 éléments
matrix = np.random.rand(10000, 10000)

start_time = time.time()

# Extraction avec découpage de base : RETOURNE UNE VUE (pas de copie)
submatrixview = matrix[::2, ::2]

durationview = time.time() - starttime
print(f"Extraction avec vue terminée en : {duration_view:.8f} secondes")
Ce même calcul prend maintenant 0,00001001 seconde. C'est 15 000 fois plus rapide .

La différence ? Quand tu utilises matrix[::2, ::2], NumPy ne copie pas les données. Il crée juste un en-tête de tableau avec de nouvelles métadonnées : une forme différente et de nouveaux strides (le nombre d'octets à sauter pour trouver l'élément suivant). C'est comme si tu avais un plan de métro : au lieu de reconstruire toutes les stations, tu changes juste les numéros des lignes. Résultat : un accès instantané, même pour des matrices géantes.

Attention cependant : une vue partage la même mémoire que l'original. Si tu modifies submatrixview, tu modifies aussi matrix. Pour éviter ça, utilise .copy() si tu veux une vraie copie indépendante.

BILAN : TROIS RÈGLES POUR DES CALCULS ULTRA-RAPIDES

Voici les trois leçons à retenir pour écrire du code NumPy performant :

  1. Vectorise : remplace les boucles par des opérations sur tableaux entiers. Utilise les fonctions NumPy natives (np.mean, np.std, etc.) et la diffusion (broadcasting).
  2. Pré-alloue : évite les tableaux temporaires en utilisant out= ou des opérateurs in-place (*=, +=).
  3. Utilise des vues : privilégie les découpages de base (arr[::2]) plutôt que les indices avancés (arr[[0,2,4]]) pour éviter les copies.
En appliquant ces trois astuces, tes pipelines de données seront plus légers, plus rapides et prêts pour la production.

NumPy n'est pas magique, mais il est incroyablement puissant si tu sais comment l'utiliser. La prochaine fois que ton code met 11 secondes à s'exécuter, rappelle-toi : avec ces trois techniques, tu peux le faire en 0,2 seconde. C'est ça, la différence entre un code qui traîne et un code qui explose.

Sources :
  • KDnuggets

L'indépendance de CLODCO est votre garantie.

Pour que l'actualité de l'IA reste sans filtre et sans concession, votre soutien est indispensable. Votre contribution est le seul moteur de notre liberté éditoriale.

Soutenir CLODCO