Aller au contenu principal

NumPy : calculer sur des milliers de mesures

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

Ce qu'on va apprendre

  • 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.
  • Résumer une série de mesures, et traiter les valeurs manquantes.
  • Reconnaître les deux erreurs qui font perdre le plus de temps : la forme et le nan.
Pourquoi ce chapitre arrive ici
Les deux chapitres suivants, Matplotlib et SciPy, travaillent sur des tableaux NumPy du début à la fin. Tracer une courbe, intégrer une fonction, résoudre une équation différentielle : tout part d'un tableau. Ce chapitre est donc le socle des deux autres, 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.

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 pour nous.
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.

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.

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.

À 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.

Vérification

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 ?

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

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

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

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

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

Synthèse

  • Un tableau a un seul type et une forme fixe ; une liste accepte tout et grandit. Le premier calcule vite, la seconde est souple.
  • Une opération écrite sur le tableau s'applique à tous les éléments. La boucle disparaît du code, et le temps de calcul avec elle.
  • * et + ne veulent pas dire la même chose sur une liste et sur un tableau. Convertir tôt.
  • Une condition sur un tableau rend un masque de booléens. tab[masque] sélectionne, masque.sum() compte, masque.mean() donne la proportion.
  • Combiner avec & et |, jamais and et or, et parenthéser chaque comparaison.
  • nan contamine les calculs exprès. nanmean et ses variantes écartent les trous, et l'employer est une décision qui se documente.
  • axis désigne la dimension qui disparaît. Vérifier la forme du résultat.
  • Devant un message d'erreur NumPy, afficher les formes avant toute chose.
  • np.where(masque) rend les indices, c'est-à-dire les instants, et non seulement les valeurs.