Atelier : NumPy et Matplotlib sur des données de course
Ce que ce chapitre apporte
- Distinguer un tableau NumPy d'une liste Python, et savoir pourquoi la distinction compte.
- Fabriquer un axe de valeurs avec np.arange et np.linspace, et choisir lequel.
- Calculer sur un tableau entier sans écrire de boucle.
- Sélectionner par masque booléen, et décider par np.where.
- Retrouver un extremum et sa position avec np.argmax.
- Produire un graphique qui porte ses unités, sa légende et ses repères.
Une écurie de course enregistre, à chaque tour, des dizaines de milliers de relevés : vitesse, régime moteur, température de freins, position. Les traiter avec des listes Python et des boucles est possible, et impraticable au-delà de quelques milliers de points.
Cet atelier installe les deux outils qui règlent la question : NumPy, qui calcule sur un tableau entier d'un seul geste, et Matplotlib, qui en fait une courbe lisible. Sept phases, chacune posant un problème de piste et une seule notion nouvelle.
Atelier à trous. Chaque bloc contient des
TODO: on remplace les..., puis on exécute. Un bloc s'exécute seul, sans voir les précédents : ce dont il a besoin est recopié en tête. Les lignesassertdu bas servent de juge. Si aucune ne proteste et que le message de validation s'affiche, la phase est faite. Une correction est dépliable sous chaque phase ; l'ouvrir avant d'avoir essayé ne coûte rien à personne d'autre qu'à soi.
Déroulé
| Durée | Phase | Notion |
|---|---|---|
| 10 min | 0. Pourquoi un tableau | Liste contre tableau, calcul vectoriel |
| 10 min | 1. Calibrer un capteur | np.arange, premier tracé |
| 15 min | 2. Distance de réaction | np.linspace, conversion d'unités |
| 15 min | 3. Distance de freinage | Puissance terme à terme, échelle non linéaire |
| 20 min | 4. Trajectoire balistique | Formule à plusieurs termes, repère au sol |
| 15 min | 5. Régime optimal | np.argmax, indice contre valeur |
| 15 min | 6. Déploiement de l'ERS | np.where, condition vectorielle |
| 15 min | 7. Régularité du pilote | Moyenne, écart-type, histogramme |
| 5 min | Vérification | Quiz et synthèse |
Compter environ deux heures. Les phases s'enchaînent, mais chaque bloc est autonome : on peut reprendre à n'importe laquelle.
Phase 0 : pourquoi un tableau
La question mérite d'être tranchée tout de suite, parce que tout le reste en dépend : qu'apporte un tableau NumPy qu'une liste Python ne sait pas faire ?
Le bloc suivant n'a rien à compléter. On l'exécute, et on regarde les deux dernières lignes.
Le même * 2 fait deux choses différentes. Sur la liste, il répète la séquence ; sur le tableau, il multiplie chaque valeur. Ce n'est pas un détail de syntaxe : c'est la différence entre une collection d'objets et un vecteur de nombres.
Sur un tableau NumPy, une opération arithmétique s'applique à tous les éléments à la fois, sans boucle écrite. v / 3.6 convertit dix mille vitesses en une expression, et le fait en une fraction du temps qu'y mettrait une boucle Python.
C'est l'unique raison d'être de NumPy, et c'est la forme que prendra chaque calcul de cet atelier : on ne parcourt pas, on applique.
Sans compléter non plus, le bloc suivant mesure l'écart sur cent mille relevés.
Le rapport exact dépend de la machine et n'a pas d'importance. Ce qui compte est son ordre de grandeur, et le fait qu'il grandit avec le nombre de points : sur les millions de relevés d'un week-end de course, l'écart cesse d'être un confort pour devenir la différence entre une analyse faite et une analyse abandonnée.
Phase 1 : calibrer un capteur
Un accéléromètre vient d'être installé. Avant de l'emmener en piste, on vérifie qu'il répond à une loi connue : sous une accélération constante , la vitesse suit .
L'axe temporel se fabrique avec np.arange(début, fin_exclue, pas), qui procède par pas fixe.
Correction
Le piège est la borne de fin : np.arange(0, 10, 0.5) s'arrête à 9,5 et ne contient que 20 points. Pour inclure 10, on demande une fin légèrement au-delà, 10.5.
Et acceleration * temps n'a besoin d'aucune boucle : c'est le calcul vectoriel de la phase 0.
Phase 2 : distance de réaction
Un pilote met environ 0,5 s à réagir. Pendant ce temps, la voiture avance sans que personne ne freine. On veut cette distance pour des vitesses d'approche de 50 à 150 km/h.
Ici, ce n'est pas un pas qu'on connaît mais un nombre de points : c'est le travail de np.linspace(début, fin_incluse, nombre).
Une distance en mètres exige une vitesse en mètres par seconde. La conversion se fait en divisant par 3,6, et elle s'applique au tableau entier d'un coup.
Correction
La courbe est une droite : la distance est proportionnelle à la vitesse, parce que le temps de réaction, lui, ne change pas. Retenir ce résultat pour la phase suivante, où la relation cessera d'être linéaire.
À 150 km/h, vingt mètres passent avant le moindre coup de frein. C'est la longueur de deux voitures et demie.
Phase 3 : distance de freinage
Le pilote a réagi ; c'est maintenant aux freins de travailler. La distance de freinage n'est pas proportionnelle à la vitesse mais à son carré : .
Correction
Le carré change tout. Doubler la vitesse de 100 à 200 km/h ne double pas la distance : il la quadruple, de 40 à 160 m. C'est la raison physique des limitations de vitesse, et elle ne se lit sur aucune moyenne, seulement sur la forme de la courbe.
Noter la comparaison avec la phase 2 : à 100 km/h, la réaction coûte 14 m et le freinage 40 m. À 250 km/h, la réaction coûte 35 m et le freinage 250 m. C'est le second terme qui explose.
Phase 4 : trajectoire balistique
En cas de sortie de piste sur un vibreur, la voiture décolle. On modélise sa trajectoire pour dimensionner les zones de dégagement : à la vitesse et sous l'angle , la hauteur en fonction de la distance vaut
np.cos(15) ne calcule pas le cosinus de quinze degrés mais celui de quinze radians, et rend une valeur parfaitement plausible, donc indétectable. La conversion se fait avec np.radians, et s'oublie une fois par promotion.
Correction
La courbe repasse sous zéro vers 127 m : c'est la portée, et le tronçon négatif n'a plus de sens physique. La voiture a touché le sol. On garde la fenêtre à 130 m pour que le franchissement se voie ; dans un vrai rapport, on tronquerait à la portée.
La formule exacte, , donne 127,4 m et sert de contrôle : si la courbe et la formule divergent, c'est presque toujours que l'angle est resté en degrés.
Phase 5 : régime optimal
Le banc d'essai donne une courbe de puissance . La puissance monte, plafonne, puis s'effondre. On veut le régime exact du pic, pour indiquer au pilote quand passer le rapport.
np.max rend la valeur la plus grande ; np.argmax rend l'indice auquel elle se trouve. C'est le second qu'il faut ici : la puissance maximale ne dit pas à quel régime elle est atteinte, mais son indice permet d'aller le chercher dans le tableau des régimes.
Correction
Le détour par l'indice est le point à retenir : idx_max ne sert à rien en lui-même, il sert à lire deux tableaux au même endroit. C'est le geste standard dès qu'on cherche « où » et pas seulement « combien ».
Le résultat se contrôle à la main : le sommet d'une parabole est en , soit exactement 5000. Que la recherche numérique et le calcul exact tombent d'accord confirme que la grille contient bien le sommet. Avec un pas de 300, elle l'aurait manqué et argmax aurait rendu le point le plus proche, sans prévenir.
Phase 6 : déploiement de l'ERS
Le système de récupération d'énergie délivre un surplus de puissance, mais il reste bloqué pendant les cinq premières secondes après un virage, pour éviter le patinage. Avant 5 s, aucun boost ; après, il monte progressivement vers son maximum.
Une condition qui doit s'appliquer à chaque instant d'un tableau ne s'écrit pas avec if : if attend une réponse unique, pas un tableau de réponses. C'est le rôle de np.where(condition, valeur_si_vrai, valeur_si_faux).
Correction
Un détail surprend à la première lecture : 50 * (1 - np.exp(-(t - 5))) est calculé pour tous les instants, y compris ceux d'avant 5 s où l'exposant est positif et le résultat aberrant. np.where ne choisit pas quoi calculer, il choisit quoi garder.
C'est sans conséquence ici. Ce le serait si la branche non retenue produisait une division par zéro ou une racine de nombre négatif, auquel cas il faudrait masquer les données avant de calculer, et non après.
Phase 7 : régularité du pilote
Le relais est terminé. Le pilote affirme avoir été régulier ; les temps au tour vont le dire. La moyenne mesure la performance, l'écart-type mesure la régularité, et c'est le second qui décide de la stratégie pneumatique.
Correction
Deux pièges dans cette phase. Le meilleur tour est le plus court, donc np.min et non np.max : la métrique va dans le sens inverse de l'intuition, et l'erreur passe inaperçue parce que le résultat reste un temps plausible.
Et np.argmin rend un indice, qui commence à zéro, alors qu'un tour de course se compte à partir de un. D'où le + 1, qui n'est pas une coquetterie : annoncer le mauvais numéro de tour à un ingénieur de piste fait analyser le mauvais tour.
Sur le fond : 0,93 s d'écart-type pour une moyenne de 80,7 s, c'est environ 1 %. Le pilote est régulier, à ceci près que le tour 7, à 83 s, sort nettement du lot. La dernière ligne l'isole par un masque booléen, et c'est la question à poser au pilote : trafic, erreur, ou drapeau jaune ?
Vérification
1.Pourquoi employer un tableau numpy plutôt qu'une liste Python pour des relevés de capteur ?
2.np.mean sur un tableau contenant un nan rend quoi ?
3.Un tableau de 1000 mesures et un tableau de 999 : que donne leur addition ?
4.np.arange(0, 10, 0.5) contient-il la valeur 10 ?
5.np.argmax rend quoi ?
6.Pourquoi tracer les données avant de calculer quoi que ce soit ?
Synthèse
- Un tableau NumPy n'est pas une liste Python : il porte un seul type, et les opérations s'y appliquent d'un coup, sur tous les éléments à la fois.
- Ce calcul vectoriel remplace la boucle :
v / 3.6convertit dix mille vitesses en une expression, et l'écart de temps grandit avec le nombre de points. np.arange(début, fin, pas)quand on connaît le pas, la borne de fin étant exclue ;np.linspace(début, fin, n)quand on connaît le nombre de points, les deux bornes étant incluses.- Les fonctions trigonométriques travaillent en radians :
np.radiansd'abord, sinon le résultat est faux sans être absurde. np.argmaxetnp.argminrendent un indice, qui sert à lire un autre tableau au même endroit. Un indice commence à zéro, un numéro de tour à un.np.where(condition, si_vrai, si_faux)applique une décision instant par instant, mais calcule les deux branches avant de choisir.- Un masque booléen (
chronos[chronos > 82]) isole ce qui sort du lot, et c'est souvent là qu'est l'information. - Un graphique d'ingénieur porte ses unités, une légende dès qu'il y a deux courbes, et les repères qui font sens : le sol, le seuil, l'instant d'activation.