Aller au contenu principal

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 lignes assert du 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éePhaseNotion
10 min0. Pourquoi un tableauListe contre tableau, calcul vectoriel
10 min1. Calibrer un capteurnp.arange, premier tracé
15 min2. Distance de réactionnp.linspace, conversion d'unités
15 min3. Distance de freinagePuissance terme à terme, échelle non linéaire
20 min4. Trajectoire balistiqueFormule à plusieurs termes, repère au sol
15 min5. Régime optimalnp.argmax, indice contre valeur
15 min6. Déploiement de l'ERSnp.where, condition vectorielle
15 min7. Régularité du piloteMoyenne, écart-type, histogramme
5 minVérificationQuiz 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.

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

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.

Le calcul vectoriel

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.

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

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 aa, la vitesse suit V=atV = a\,t.

L'axe temporel se fabrique avec np.arange(début, fin_exclue, pas), qui procède par pas fixe.

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

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

La télémétrie enregistre en m/s, le tableau de bord affiche des km/h

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.

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

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é : D=kV2D = k\,V^2.

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

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 V0V_0 et sous l'angle α\alpha, la hauteur en fonction de la distance vaut

y(x)=xtan(α)gx22(V0cosα)2y(x) = x \tan(\alpha) - \frac{g\,x^2}{2\,(V_0 \cos \alpha)^2}

NumPy raisonne en radians

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.

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

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, V02sin(2α)/gV_0^2 \sin(2\alpha)/g, 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 P(R)=aR2+bR+cP(R) = aR^2 + bR + c. La puissance monte, plafonne, puis s'effondre. On veut le régime exact du pic, pour indiquer au pilote quand passer le rapport.

Indice ou valeur

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.

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

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 b/2a-b/2a, 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).

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

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.

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

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

Vérification rapideon peut se reprendre

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.6 convertit 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.radians d'abord, sinon le résultat est faux sans être absurde.
  • np.argmax et np.argmin rendent 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.