Aller au contenu principal

NumPy : calculer sur des milliers de mesures

Ce que ce chapitre apporte 10 points
  • Distinguer un tableau NumPy d'une liste Python, et savoir quand chacun convient.
  • Calculer sur tout un tableau sans écrire de boucle.
  • Sélectionner des valeurs par une condition plutôt que par leur position.
  • Lire un fichier de mesures directement en tableau.
  • Résumer une série de mesures : moyenne, médiane, écart-type, quartiles, effectifs et cumuls, et savoir où NumPy ne suit pas les conventions du cours de statistiques.
  • Traiter les valeurs manquantes.
  • Tirer des nombres au hasard de façon reproductible.
  • Comprendre la diffusion, et savoir pourquoi (3, 4) et (4,) s'entendent quand (3, 4) et (3,) échouent.
  • Distinguer une vue d'une copie en découpant un tableau.
  • Reconnaître les deux erreurs qui font perdre le plus de temps : la forme et le nan.

Une liste Python sait tout faire, et c'est bien son problème : elle peut contenir un nombre, un texte et une autre liste en même temps, donc Python doit vérifier le type de chaque élément à chaque opération. Un tableau NumPy à deux dimensions est une matrice au sens du chapitre de mathématiques sur les matrices et les systèmes linéaires, et les opérations qu'il propose en sont la traduction directe. Sur dix valeurs, personne ne le remarque. Sur un million de relevés de capteur, cela devient la différence entre une seconde et une minute. NumPy propose l'autre marché : un seul type pour tout le tableau, et des opérations qui s'appliquent d'un coup.

Pourquoi ce chapitre arrive ici
Les chapitres qui suivent, Matplotlib, pandas et SciPy, travaillent sur des tableaux NumPy du début à la fin. Tracer une courbe, résumer une colonne, intégrer une fonction, résoudre une équation différentielle : tout part d'un tableau. Ce chapitre est donc leur socle, et non un supplément.

Le tableau, et ce qu'il échange

main.py
Sortie
>_ Prêt à exécuter…

Trois différences avec une liste, et elles découlent toutes de la même contrainte.

Un seul type pour tout le tableau. dtype le donne. NumPy peut donc ranger les valeurs les unes derrière les autres en mémoire, sans indirection, et le processeur les lit d'un bloc.

Une taille fixée. Ajouter un élément à un tableau demande d'en recopier tout le contenu ailleurs. Un tableau se construit d'un coup, il ne se remplit pas au fil de l'eau comme une liste.

Une forme. shape dit combien de lignes et de colonnes. Un relevé de capteur est un tableau à une dimension, une image en niveaux de gris un tableau à deux.

Le piège qui fait perdre une demi-heure la première fois
Le même opérateur ne fait pas la même chose sur une liste et sur un tableau, et rien ne signale la différence.
main.py
Sortie
>_ Prêt à exécuter…

Sur une liste, * répète et + concatène. Sur un tableau, les deux calculent. Convertir en tableau change donc le sens du code déjà écrit, et c'est une bonne raison de convertir tôt plutôt que tard.

Vérification rapideon peut se reprendre

1.Pourquoi un tableau NumPy est-il plus rapide qu'une liste sur un million de valeurs ?

2.liste * 2 et tableau * 2 donnent quoi ?

Fabriquer un tableau

Un tableau se construit rarement à la main. Quatre fonctions couvrent presque tous les cas, et les deux dernières reviennent dans chaque chapitre qui suit.

AppelCe qu'il produit
np.zeros(5)cinq zéros, pour un tableau qu'on remplira
np.ones((2, 3))une grille de 1, la forme se donne en couple
np.arange(0, 10, 2)comme range, mais en tableau : 0, 2, 4, 6, 8
np.linspace(0, 1, 5)cinq valeurs régulièrement réparties de 0 à 1, bornes comprises
main.py
Sortie
>_ Prêt à exécuter…

La différence entre arange et linspace est celle du pas et du nombre de points. arange reçoit le pas et la borne haute est exclue ; linspace reçoit le nombre de valeurs et inclut les deux bornes. Pour tracer une courbe, c'est linspace qu'on veut : le nombre de points est précisément ce qu'on règle.

reshape réarrange les mêmes valeurs dans une autre forme, sans en recalculer aucune. Le produit des dimensions doit rester le même, sinon NumPy refuse.

Calculer sans boucle

C'est la raison d'être de NumPy. Une opération écrite sur le tableau entier s'applique à chaque élément, et la boucle disparaît du code.

main.py
Sortie
>_ Prêt à exécuter…

La ligne (mv - 500) / 10 traite les huit valeurs. Elle en traiterait un million sans changer d'un caractère, et sans que la durée d'exécution soit multipliée par le nombre de tours d'une boucle Python.

La règle de lecture. Devant du code NumPy, se demander non pas « que fait cette ligne à un élément » mais « que fait cette ligne au tableau entier ». C'est le changement de point de vue qui coûte le plus au début, et celui qui rend le reste évident.

Le gain, mesuré

main.py
Sortie
>_ Prêt à exécuter…

L'écart n'est pas une optimisation de confort. Sur un jeu de mesures réel, il décide de ce qui est faisable dans la séance et de ce qui ne l'est pas, exactement comme au chapitre sur la complexité.

Sélectionner par une condition

Voici la seconde idée du chapitre, et celle qui change le plus la façon d'écrire. Plutôt que de parcourir en testant, on écrit la condition sur le tableau entier et l'on obtient un masque : un tableau de True et de False, de la même forme.

main.py
Sortie
>_ Prêt à exécuter…

Deux détails qui rendent ce mécanisme plus utile qu'il n'y paraît. sum() sur un masque compte les cas vrais, puisque True vaut 1. Et mean() sur un masque donne directement la proportion, ce qui évite une division.

Les conditions se combinent, avec une syntaxe qui surprend une fois :

main.py
Sortie
>_ Prêt à exécuter…
and et or ne fonctionnent pas sur les tableaux
Écrire (a > 1) and (b < 2) sur des tableaux lève une erreur explicite : « The truth value of an array with more than one element is ambiguous ». Python demande à convertir le tableau en un seul booléen, et NumPy refuse de choisir.
La raison est bonne : un tableau de dix valeurs dont trois vérifient la condition n'est ni vrai ni faux. Employer & et |, qui travaillent terme à terme, et parenthéser chaque comparaison parce que ces opérateurs sont plus prioritaires que >.

Les valeurs manquantes

Un capteur débranché, une trame perdue, une ligne vide dans le fichier : les mesures réelles ont des trous. NumPy les note nan, pour not a number, et son comportement est délibéré.

main.py
Sortie
>_ Prêt à exécuter…

Pourquoi la moyenne rend nan, et pourquoi c'est la bonne réponse. Une moyenne calculée en ignorant silencieusement les mesures manquantes serait un mensonge : elle porterait sur un échantillon différent de celui qu'on croit, sans que rien ne le dise. NumPy propage donc le nan jusqu'au résultat.
Les fonctions nanmean, nanstd, nanmax font le calcul en écartant les trous. Les employer est légitime ; le faire sans le savoir ne l'est pas. C'est pourquoi il faut deux noms de fonction et non un réglage par défaut.

Vérification rapideon peut se reprendre

1.(a > 1) and (b < 2) sur des tableaux produit quoi ?

2.Sur un masque de booléens, que donne .sum() ?

3.mesures.mean() sur un tableau contenant un nan rend quoi ?

Deux dimensions

Un tableau à deux dimensions range des lignes et des colonnes : quatre capteurs relevés sur cinq instants, par exemple.

main.py
Sortie
>_ Prêt à exécuter…
axis, la source de confusion numéro un
axis=0 parcourt les lignes et rend un résultat par colonne. axis=1 parcourt les colonnes et rend un résultat par ligne.
Le moyen de ne plus se tromper : l'axe indiqué est celui qui disparaît. Sur une forme (4, 5), axis=1 supprime la seconde dimension et laisse quatre valeurs, une par capteur. Vérifier la forme du résultat coûte une seconde et tranche à coup sûr.

La diffusion : quand deux formes différentes s'entendent

La section précédente laisse une question ouverte. Retirer une valeur unique de tout un tableau fonctionne, mv - 500, alors que les deux formes diffèrent. Et retrancher une moyenne par capteur à une grille de relevés fonctionne parfois, échoue d'autres fois, apparemment sans logique.

La logique existe, et elle tient en trois lignes. Les formes s'alignent par la droite, la plus courte se complétant par des 1 à gauche. Deux dimensions s'entendent si elles sont égales, ou si l'une vaut 1. Une dimension de taille 1 est alors étirée jusqu'à l'autre.

C'est l'alignement par la droite qui surprend, et la figure suivante le montre : un vecteur de la taille des colonnes s'ajoute à une matrice, un vecteur de la taille des lignes est refusé.

releves(3, 4)34
correction(4)14
résultat34
le second est étirétailles égales

forme du résultat (3, 4)

releves

11121314
21222324
31323334
+

correction

11121314
11121314
11121314

Les cases estompées sont relues, pas recopiées : NumPy repasse sur la même valeur autant de fois qu'il le faut, et n'occupe pas un octet de plus.

Une correction par colonne, appliquée aux trois lignes. Les formes s'alignent par la droite, et la dimension absente vaut 1, donc s'étire.
À manipuler
Afficher les valeurs et regarder la seconde grille : la même ligne s'y répète vers le bas, estompée, parce qu'elle est relue et non recopiée. C'est ce qui rend la diffusion gratuite en mémoire : soustraire une correction à un million de lignes ne fabrique pas un million de copies de la correction.

Le cas symétrique échoue, et c'est le piège le plus fréquent du chapitre :

releves(3, 4)34
moyennes(3)13
résultat3✗
le second est étiréaucune ne vaut 1 : refus

operands could not be broadcast together

La même opération avec un vecteur de la taille des lignes. L'alignement par la droite confronte 4 et 3, dont aucune ne vaut 1, et NumPy refuse.

Retrancher une moyenne par ligne demande donc de rendre à ce vecteur une seconde dimension, pour qu'il s'aligne en colonne :

main.py
Sortie
>_ Prêt à exécuter…

L'écriture moyennes[:, np.newaxis] fait la même chose que reshape(3, 1) et se rencontre partout ; keepdims=True sur le mean l'évite en rendant directement la forme (3, 1).

Les erreurs de forme

main.py
Sortie
>_ Prêt à exécuter…

NumPy refuse d'aligner deux tableaux de tailles incompatibles, et c'est une bonne nouvelle. Un relevé décalé d'une ligne, un fichier tronqué, deux séries qui ne couvrent pas la même période : l'erreur apparaît immédiatement, à l'endroit du problème, plutôt que de produire un résultat plausible et faux.

Le premier réflexe devant une erreur NumPy
Afficher les formes. print(a.shape, b.shape) résout la grande majorité des messages incompréhensibles, et bien plus vite que de relire le code.
Vérification rapideon peut se reprendre

1.Sur un tableau de forme (4, 5), que rend mean(axis=1) ?

2.Un tableau de forme (3, 4) et un vecteur de forme (4,) s'additionnent-ils ?

3.Additionner un tableau de 1000 valeurs et un de 999 donne quoi ?

Découper : une vue, pas une copie

Un tableau se découpe comme une liste, tab[1:4], et sur deux dimensions avec une virgule, grille[0:2, 1:3]. Une différence essentielle sépare pourtant les deux : une découpe de liste est une copie, une découpe de tableau est une vue.

main.py
Sortie
>_ Prêt à exécuter…

Ce n'est pas une bizarrerie : c'est la même raison que pour la diffusion. Une vue ne coûte rien, quelle que soit la taille du tableau, alors qu'une copie systématique doublerait la mémoire à chaque découpe. Sur un relevé d'un million de points, extraire une fenêtre glissante serait impraticable autrement.

La règle pratique en découle : .copy() dès qu'on va modifier une découpe dont on veut préserver l'original. C'est exactement la précaution du chapitre sur les fonctions, pour exactement la même raison.

Deux découpages reviennent constamment sur une grille :

main.py
Sortie
>_ Prêt à exécuter…

Le deux-points seul signifie « toute cette dimension ». grille[:, 2] se lit donc « toutes les lignes, colonne 2 ».

Vérification rapideon peut se reprendre

fenetre = mesures[1:4] puis fenetre[0] = 999 : que devient mesures ?

Depuis un fichier de mesures

Un relevé arrive le plus souvent dans un fichier texte, une ligne par instant, une colonne par grandeur. np.loadtxt le lit directement en tableau, sans boucle ni conversion à la main. Ici, le fichier est simulé par un texte en mémoire, io.StringIO, parce que l'éditeur du navigateur n'a pas de disque ; sur une machine, on écrit simplement le nom du fichier à la place.

main.py
Sortie
>_ Prêt à exécuter…

delimiter donne le séparateur de colonnes, skiprows=1 saute la ligne d'en-tête. Le résultat est un tableau à deux dimensions, une ligne par instant ; donnees[:, 0] en extrait la première colonne, l'écriture détaillée plus haut avec les découpes. Si le fichier contient des cases vides, np.loadtxt échoue : np.genfromtxt les remplace par nan, ce qui ramène aux valeurs manquantes traitées plus haut. Il prend les mêmes arguments à un près, et celui-là se venge : l'en-tête se saute par skip_header=1, jamais par skiprows, qu'il refuse par une TypeError.

Résumer une série

Calculer sur un tableau entier sert d'abord à le résumer : où se situent les valeurs, et à quel point elles s'étalent. Le chapitre de statistiques définit chaque indicateur et dit lequel choisir ; cette section montre comment NumPy les calcule, et les trois endroits où il ne fait pas ce qu'on croit.

La position : moyenne, médiane, extrêmes

Dix temps de réponse d'un service, dont une requête qui a expiré : la situation que le chapitre de statistiques prend pour exemple.

main.py
Sortie
>_ Prêt à exécuter…

La moyenne vaut 441,4 ms et la médiane 46,5 ms : une seule valeur a suffi à tirer la moyenne à dix fois la durée d'une requête ordinaire. C'est la raison pour laquelle on publie la médiane d'un temps de réponse, et NumPy la donne aussi facilement que la moyenne.

argmin et argmax ne rendent pas la valeur extrême mais sa position dans le tableau, ici 2 : c'est elle qui permet de remonter à la requête fautive, à son heure, à son client. Comme partout en Python, la position se compte à partir de 0.

La médiane n'est pas une méthode
t.mean(), t.sum(), t.min() et t.std() s'écrivent comme des méthodes du tableau, mais t.median() déclenche une AttributeError. La médiane s'obtient par la fonction np.median(t), et il en va de même pour np.percentile. Les deux écritures se valent, np.mean(t) fonctionne aussi : dans le doute, la forme np.fonction(t) marche toujours.

La dispersion : variance, écart-type, quartiles

main.py
Sortie
>_ Prêt à exécuter…

Deux lignes de la sortie méritent qu'on s'y arrête, parce qu'elles donnent chacune deux nombres pour une seule notion.

NumPy divise par N, pandas par N − 1
Par défaut, std() et var() divisent par l'effectif N : c'est l'écart-type qui décrit la série qu'on a sous les yeux. Pour estimer l'écart-type d'une population à partir d'un échantillon, et donc pour un intervalle de confiance, il faut diviser par N − 1, ce qu'on demande par ddof=1. La bibliothèque pandas fait le choix inverse : serie.std() divise par N − 1. Le même calcul donne donc 13,26 d'un côté et 13,85 de l'autre, et l'écart grandit à mesure que l'effectif diminue. Écrire ddof explicitement lève l'ambiguïté.
np.percentile n'applique pas la règle du cours
np.percentile interpole entre deux rangs : il trouve Q₁ = 17,25 et Q₃ = 31,5, là où la règle des deux moitiés donne 16,5 et 32. Aucun des deux n'est faux, ce sont deux conventions, et elles se rejoignent dès que la série est longue. Mais un résultat vérifié à la main doit l'être avec la même règle : la petite fonction quartiles_du_cours ci-dessus l'applique, et c'est aussi celle des boîtes à moustaches tracées dans les chapitres du site. Les boîtes de plt.boxplot, elles, suivent np.percentile.

Trier, compter, cumuler

Le tableau d'effectifs du chapitre de statistiques, avec ses fréquences et ses cumuls, se construit en trois appels.

main.py
Sortie
>_ Prêt à exécuter…

np.unique rend les modalités distinctes, triées, et avec return_counts=True le nombre de fois où chacune apparaît. np.cumsum fait la somme cumulée : sa dernière case vaut l'effectif total, le contrôle qui prend trois secondes. Et le mode se lit en combinant les deux : la modalité placée au même rang que le plus grand effectif.

np.sort rend une copie triée et laisse l'original intact, comme sorted sur une liste. Pour regrouper une variable continue en classes, np.histogram(x, bins=5) rend les effectifs et les bornes des cinq classes : c'est le calcul que plt.hist fait avant de dessiner.

Tout accepte axis.

mean, median, std, min, argmax, percentile, sort et cumsum acceptent tous le paramètre axis, présenté plus haut avec les tableaux à deux dimensions : la même ligne résume un capteur ou tous les capteurs d'un coup.

Vérification rapideon peut se reprendre

1.Un tableau de temps de réponse contient une valeur de 4 000 ms parmi des valeurs voisines de 45 ms. Quelle commande donne la valeur typique ?

2.Pour un intervalle de confiance sur la moyenne d'un échantillon, quel écart-type calculer ?

Tirer au hasard

Tester un traitement demande des données, et les vraies ne sont pas toujours là. NumPy sait en fabriquer : des relevés bruités, des dés, des tirages dans une liste. L'exercice qui suit, le chapitre Matplotlib et le parcours d'intelligence artificielle s'en servent tous, toujours de la même façon.

main.py
Sortie
>_ Prêt à exécuter…
AppelCe qu'il tire
rng.random(n)n réels uniformes dans [0;1[[0 ; 1[
rng.integers(a, b, size=n)n entiers de a à b exclu, comme range : un dé s'écrit integers(1, 7)
rng.normal(mu, sigma, size=n)n valeurs d'une loi normale de moyenne mu et d'écart-type sigma
rng.choice(liste, size=n, p=...)n éléments de la liste, avec les probabilités p si on les donne

Le nombre passé à default_rng, la graine, fixe toute la suite des tirages : relancer le bloc redonne exactement les mêmes nombres. C'est ce qui rend un résultat vérifiable, et c'est pour cela que tous les exemples du site en posent une. Sans graine, np.random.default_rng() change de tirage à chaque exécution, ce qui est voulu pour une simulation et gênant pour un exercice corrigé.

Deux écritures pour la même chose
Beaucoup de code en ligne emploie encore l'ancienne interface, np.random.seed(7) suivie de np.random.rand() ou np.random.randint(). Elle fonctionne, mais elle repose sur un générateur unique et partagé par tout le programme. La documentation de NumPy recommande le générateur explicite, rng = np.random.default_rng(graine), et c'est lui qu'emploie ce parcours.

Mettre en pratique

Le contrôle qualité. Un capteur relève une pression toutes les minutes. Écrire le code qui compte les valeurs hors de l'intervalle [980, 1040], en donne la proportion, et affiche la plus grande dérive observée.

main.py
Sortie
>_ Prêt à exécuter…
Afficher la solution
main.py
Sortie
>_ Prêt à exécuter…

np.where rend les indices où le masque est vrai, ce qui permet de remonter à l'instant du relevé et non seulement à sa valeur. C'est presque toujours cette information-là qu'on cherche ensuite.

Chapitre révisé en octobre 2026 · Une erreur dans ce chapitre ?

Dans la bibliothèque d'exercices