La performance NumPy se gagne surtout en évitant trois erreurs bêtes : les boucles Python, les tableaux temporaires et les copies mémoire inutiles. Je vais droit au sujet : vectoriser, écrire au bon endroit, et manipuler des vues. C’est moins glamour qu’un gros serveur, mais souvent beaucoup plus efficace.
Pourquoi les boucles Python ralentissent NumPy ?
Les boucles Python ralentissent NumPy parce qu’elles traitent les éléments un par un côté interpréteur, alors que NumPy est fait pour déléguer les calculs à des fonctions optimisées en C.
Quand j’écris une boucle Python explicite, Python repasse à chaque élément, vérifie les types, gère l’itération, appelle le bytecode… Bref, il fait beaucoup de petites choses pas très sexy. Une opération vectorisée NumPy, elle, prend tout le tableau et lance une routine bas niveau optimisée. C’est là que NumPy devient vraiment rapide.
Attention au piège classique : np.vectorize ne rend pas le calcul vraiment vectorisé. C’est surtout une commodité d’écriture. Ça masque une boucle Python derrière une syntaxe plus propre, mais ce n’est pas comparable aux vraies ufuncs, les fonctions universelles NumPy comme np.add, np.sqrt, np.exp ou les opérations directes avec +, –, /.
Les ufuncs appliquent une opération élémentaire sur des tableaux entiers avec du code C optimisé. Le CPU travaille mieux, les accès mémoire sont plus réguliers, et quand les conditions sont bonnes, NumPy peut profiter d’optimisations comme SIMD, c’est-à-dire traiter plusieurs valeurs en une seule instruction processeur. Oui, le CPU aussi aime les lots.
import numpy as np
import time
rng = np.random.default_rng(42)
X = rng.normal(size=(50000, 1000))
n_rows, n_cols = X.shape
Z = np.empty_like(X)
start = time.perf_counter()
for j in range(n_cols):
col = X[:, j]
mu = sum(col) / n_rows
sigma = (sum((v - mu) ** 2 for v in col) / n_rows) ** 0.5
for i in range(n_rows):
Z[i, j] = (X[i, j] - mu) / sigma
elapsed = time.perf_counter() - start
print(elapsed)
Sur mon test, cette version en boucles tourne autour de 10,9986 s. La version NumPy fait le même travail, mais elle laisse NumPy calculer les moyennes et écarts-types par colonne.
start = time.perf_counter()
mean = X.mean(axis=0)
std = X.std(axis=0)
Z = (X - mean) / std
elapsed = time.perf_counter() - start
print(elapsed)
Résultat mesuré : environ 0,1972 s. Ça fait un gain d’environ 56x. Même machine, même matrice, juste une autre façon de parler à NumPy.
Le broadcasting, ici, est simple : X a une forme (50000, 1000), et mean a une forme (1000,). NumPy comprend que les 1000 moyennes correspondent aux 1000 colonnes. Il les “étend” conceptuellement sur les 50000 lignes, sans copier 50000 fois le tableau des moyennes. C’est propre, rapide, et franchement plus lisible.
| Approche | Ce que ça fait vraiment | Performance |
| Boucle Python | Traite les valeurs une par une côté interpréteur | Lent sur gros tableaux |
| np.vectorize | Cache une boucle Python derrière une syntaxe pratique | Pas une vraie optimisation |
| Ufuncs avec broadcasting | Délègue le calcul à NumPy en bas niveau | Très rapide quand la mémoire suit |
Comment éviter les allocations temporaires ?
On évite les allocations temporaires en écrivant dans des tableaux existants avec des opérations en-place et le paramètre out des ufuncs NumPy.
Prenons un cas tout bête : y = 2 * x + 3. Ça a l’air propre, lisible, presque innocent. Sauf que NumPy va d’abord créer un tableau temporaire pour 2 * x, puis créer un autre tableau pour ajouter 3. Sur un petit tableau, franchement, on s’en fiche. Sur 10, 50 ou 100 millions de valeurs, ça commence à taper dans la RAM, dans le cache CPU, et dans le temps d’exécution. Ce n’est pas une fuite mémoire, c’est juste du travail inutile, ce qui est presque plus vexant.
La première option, c’est l’opération en-place. Ça veut dire qu’on modifie directement le tableau existant, sans créer un nouveau résultat à chaque étape.
x *= 2
x += 3
Ici, x est modifié directement. C’est efficace, mais il y a un vrai piège : il faut avoir le droit de toucher aux données d’origine. Si x sert encore ailleurs, ou si vous devez garder sa valeur initiale, ne faites pas ça. Préparez plutôt un tableau résultat séparé.
La deuxième option, souvent plus propre, c’est le paramètre out. Les ufuncs NumPy, c’est-à-dire les fonctions vectorisées comme np.add, np.multiply ou np.sqrt, acceptent souvent out pour écrire directement dans un tableau déjà alloué.
import numpy as np
y = np.empty_like(x)
np.multiply(x, 2, out=y)
np.add(y, 3, out=y)
Là, je contrôle exactement où va le résultat. Je crée y une seule fois, puis je réutilise sa mémoire. Si je veux aller encore plus loin et modifier x directement, je peux faire la variante totalement en-place.
np.multiply(x, 2, out=x)
np.add(x, 3, out=x)
En pratique, je garde l’expression classique pour les petits volumes et quand la lisibilité prime. J’utilise l’en-place quand le tableau peut être modifié sans danger. J’utilise out quand je veux contrôler précisément l’emplacement du résultat. Cette logique devient vraiment utile avec de grands tableaux numériques, des features ML, des matrices de simulation ou des données analytiques massives.
| Méthode | Lisibilité | Consommation mémoire | Risque principal |
| Expression classique | Très bonne | Plus élevée | Allocations temporaires invisibles |
| En-place | Bonne | Faible | Modifier les données d’origine par erreur |
| Paramètre out | Moyenne | Très contrôlée | Code un peu plus verbeux |
Le bon code NumPy n’est pas toujours le plus court. C’est celui qui évite de faire bosser la machine pour rien.
Quand une vue NumPy vaut mieux qu’une copie ?
Une vue NumPy vaut mieux qu’une copie quand on veut lire ou transformer une partie d’un tableau sans dupliquer les données en mémoire.
Une vue mémoire, c’est un nouvel objet tableau qui pointe vers les mêmes données sous-jacentes. Il peut avoir une forme différente, des strides différents, c’est-à-dire la manière de se déplacer en mémoire, ou un décalage de départ. Une copie, elle, recrée un nouveau bloc mémoire avec les données recopiées. La vue regarde le même carton. La copie refait le carton complet. Et parfois, le carton est très gros.
Les cas classiques qui renvoient souvent des vues sont simples à reconnaître :
- x[100:200] prend une tranche continue.
- X[:, 0] récupère une colonne.
- X[::2] prend un élément sur deux.
Le piège, c’est qu’une vue reste liée au tableau d’origine. C’est excellent pour la performance, parce qu’on évite une allocation mémoire inutile. Mais si on modifie la vue sans y penser, le tableau parent change aussi. Et là, NumPy ne vous demandera pas confirmation, évidemment.
import numpy as np
x = np.arange(10)
v = x[2:5]
v[:] = 99
print(x)
print(v)
Ici, x devient [0 1 99 99 99 5 6 7 8 9]. Si je veux une tranche indépendante, je force la copie avec .copy().
x = np.arange(10)
v = x[2:5].copy()
v[:] = 99
print(x)
print(v)
Autre point important : la contiguïté mémoire. Un tableau est contigu quand ses données sont rangées sans trous dans l’ordre attendu. En ordre C, les lignes sont stockées en priorité. En ordre F, comme Fortran, les colonnes passent d’abord. Certaines opérations sont très rapides si les données sont dans le bon ordre, et plus lentes si NumPy doit sauter partout en mémoire.
Pour vérifier ça, j’utilise souvent ces attributs et fonctions :
- arr.flags, avec C_CONTIGUOUS et F_CONTIGUOUS, pour voir le rangement mémoire.
- arr.base, pour savoir si un tableau référence un autre tableau.
- np.shares_memory(a, b), pour tester le partage mémoire.
- arr.nbytes, pour mesurer la taille brute des données.
| Cas | Vue ou copie généralement | Point d’attention performance |
| Slicing simple | Vue | Très rapide, mais modification visible dans le parent. |
| Copie explicite | Copie | Plus sûr, mais coûte mémoire et temps. |
| Reshape possible en vue | Vue | Rapide si la mémoire permet juste de changer la forme. |
| Tableau transposé | Vue souvent | Peut devenir moins contigu, donc plus lent ensuite. |
| Extraction avancée | Copie | Pratique, mais allocation mémoire souvent cachée. |
Comment choisir la bonne optimisation NumPy ?
La bonne optimisation NumPy dépend d’abord du goulet d’étranglement : CPU Python, allocations mémoire ou copies inutiles.
Si le code passe son temps dans des boucles Python, surtout des for qui parcourent les éléments d’un ndarray, je regarde d’abord la vectorisation avec les ufuncs et le broadcasting. Une ufunc, c’est une fonction NumPy optimisée en C, comme np.add, np.sqrt ou np.maximum. Le broadcasting, c’est la capacité de NumPy à appliquer une opération entre tableaux de formes compatibles sans dupliquer les données. Très pratique, tant qu’on ne fabrique pas un monstre mémoire sans le voir.
Si la RAM grimpe fort, là je me méfie des expressions composées. Une ligne élégante peut créer plusieurs tableaux temporaires. C’est joli, mais parfois ça chauffe. Dans ce cas, les opérations in-place et le paramètre out peuvent aider. Si je vois des .copy() posés “au cas où”, je les questionne. C’est souvent un petit sabotage poli. Si une vue suffit, autant ne pas copier. Et si un tableau est non contigu en mémoire, certaines opérations peuvent ralentir, parce que le CPU aime lire les données bien rangées. Comme nous, finalement.
| Symptôme | Piste à vérifier |
| Boucle for sur chaque élément | Vectoriser avec ufuncs ou broadcasting |
| RAM qui explose | Réduire les temporaires, utiliser out |
| .copy() fréquent | Remplacer par une vue si c’est sûr |
| Performance étrange | Vérifier la contiguïté mémoire |
Je mesure toujours avant et après. Avec time.perf_counter() dans un script, ou %timeit dans un notebook. Et je mesure sur des données représentatives, pas sur un tableau minuscule qui tient dans un coin de cache CPU et raconte n’importe quoi. C’est souvent là que les benchmarks maison partent en freestyle.
import numpy as np
import time
x = np.random.rand(10_000_000)
y = np.random.rand(10_000_000)
t0 = time.perf_counter()
z = np.sqrt(x * x + y * y)
t1 = time.perf_counter()
out = np.empty_like(x)
t2 = time.perf_counter()
np.multiply(x, x, out=out)
np.add(out, y * y, out=out)
np.sqrt(out, out=out)
t3 = time.perf_counter()
print("Expression classique:", t1 - t0)
print("Version avec out:", t3 - t2)
Ce benchmark n’annonce pas un gain magique. Le résultat dépend de la taille des données, de la machine, du dtype, c’est-à-dire le type numérique comme float64 ou int32, de la contiguïté mémoire et de l’opération elle-même.
arr = np.random.rand(1_000_000).astype(np.float64)
print(arr.nbytes) # Taille en octets
print(arr.nbytes / 1e6) # Taille approximative en Mo
- Vectoriser d’abord, surtout quand Python boucle élément par élément.
- Éviter np.vectorize pour la performance, c’est surtout une commodité d’écriture.
- Utiliser le broadcasting proprement, sans créer des tableaux géants par accident.
- Réduire les temporaires avec out ou des opérations in-place quand c’est lisible.
- Préférer les vues quand c’est sûr, et les copies seulement quand elles sont nécessaires.
- Mesurer avant/après, parce qu’on optimise pour enlever du gaspillage, pas pour gagner un concours de code cryptique.
Et si votre vrai gain venait juste du bon tableau ?
La performance NumPy vient rarement d’une astuce magique. Elle vient surtout d’un bon usage du modèle NumPy : envoyer le calcul aux ufuncs, laisser le broadcasting faire le boulot sans dupliquer les données, limiter les allocations temporaires avec l’in-place et out, puis éviter les copies quand une vue suffit. Le gros piège, c’est d’écrire du Python qui ressemble à du NumPy mais qui garde les vieux réflexes de boucle. En mesurant proprement, vous repérez vite où ça coince. Le bénéfice est simple : des calculs plus rapides, moins de RAM consommée, et du code numérique qui tient mieux la charge.
FAQ
- Pourquoi NumPy est plus rapide que les boucles Python ?
NumPy est plus rapide quand on utilise ses opérations natives parce qu’elles exécutent le calcul dans du code bas niveau optimisé, souvent en C, au lieu de passer élément par élément dans l’interpréteur Python. Le gain devient énorme sur de grands tableaux, comme une matrice de plusieurs millions de valeurs. - Est-ce que np.vectorize améliore vraiment les performances ?
Pas vraiment. np.vectorize rend parfois le code plus agréable à écrire, mais il ne transforme pas une fonction Python en vraie opération NumPy optimisée. Pour la performance, il vaut mieux utiliser les ufuncs natives, les opérations de tableau et le broadcasting. - À quoi sert le paramètre out dans NumPy ?
Le paramètre out permet d’écrire le résultat d’une ufunc dans un tableau déjà existant. Ça évite certaines allocations temporaires, surtout sur les grands tableaux. C’est très utile quand la mémoire compte ou quand une expression crée trop d’intermédiaires. - Quelle différence entre une vue et une copie NumPy ?
Une vue référence les mêmes données en mémoire que le tableau d’origine. Une copie crée un nouveau bloc mémoire avec les données dupliquées. La vue est plus légère et souvent plus rapide, mais si vous la modifiez, vous pouvez aussi modifier le tableau parent. C’est puissant, mais il faut le savoir. - Comment savoir quoi optimiser dans un code NumPy ?
Je commence par mesurer. Si le temps part dans des boucles Python, je vectorise. Si la RAM explose, je cherche les temporaires et j’utilise l’in-place ou out. Si les données sont dupliquées inutilement, je vérifie les vues, les copies, arr.base, np.shares_memory et la contiguïté mémoire.
A propos de l’auteur
Je suis Franck Scandolera, expert et formateur en Data, IA, tracking server-side, Analytics Engineering, automatisation No/Low Code avec n8n, SEO/GEO et intégration de l’IA en entreprise. J’accompagne des équipes qui manipulent de la donnée au quotidien, parfois propre, parfois beaucoup moins, soyons honnêtes. Je dirige l’agence webAnalyste et l’organisme Formations Analytics. J’ai travaillé avec des références comme Logis Hôtel, Yelloh Village, BazarChic, la Fédération Française de Football ou Texdecor. Si vous voulez industrialiser vos traitements data, automatiser vos workflows ou fiabiliser vos analyses, contactez-moi.
⭐ Data Analyst, Analytics Engineer et expert dans l’automatisation IA ⭐
Ref clients : Logis Hôtel, Yelloh Village, BazarChic, Fédération Football Français, Texdecor…
Mon terrain de jeu :
Data Analyst & Analytics engineering : tracking propre RGPD, entrepôt de données (GTM server, BigQuery…), modèles (dbt/Dataform), dashboards décisionnels (Looker, SQL, Python).
Automatisation IA des taches Data, Marketing, RH, compta etc : conception de workflows intelligents robustes (n8n, Make, App Script, scraping) connectés aux API de vos outils et LLM (OpenAI, Mistral, Claude…).
Engineering IA pour créer des applications et agent IA sur mesure : intégration de LLM (OpenAI, Mistral…), RAG, assistants métier, génération de documents complexes, APIs, backends Node.js/Python.





