Aller au contenu principal

Atelier : NumPy et Matplotlib sur des données de course

Ce que ce chapitre apporte 6 points
  • 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 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 moins un quart. Les phases s'enchaînent, mais chaque bloc est autonome : on peut reprendre à n'importe laquelle.

Avant de commencer

Cet atelier ne présente aucune notion nouvelle : il fait travailler celles des deux chapitres qui le précèdent, NumPy et Matplotlib, sur un problème complet. Deux choses en sont supposées acquises, et il vaut mieux les avoir sous la main.

Un tableau n'est pas une liste. Sur un tableau NumPy, une opération arithmétique s'applique à tous les éléments à la fois : v / 3.6 convertit dix mille vitesses en une expression. Sur une liste Python, v * 2 répète la séquence au lieu de doubler ses valeurs. C'est la section « Pourquoi un tableau » du chapitre NumPy.

Un graphique porte ses unités. Chaque tracé de cet atelier attend un axe nommé, une légende quand il y a deux courbes, et un titre qui dit ce qu'on regarde. Le chapitre Matplotlib en donne la liste.

Ce qui est nouveau ici, c'est de choisir soi-même l'outil devant un problème de piste, ce qu'aucun exemple de cours ne fait faire.

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=a tV = 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 du chapitre sur NumPy, appliqué à tout le tableau d'un geste.

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=k V2D = 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⁡(α)−g x22 (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.

Vérification rapideon peut se reprendre

1.np.mean sur un tableau contenant un nan rend quoi ?

2.Un tableau de 1000 mesures et un tableau de 999 : que donne leur addition ?

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.

Vérification rapideon peut se reprendre

1.np.arange(0, 10, 0.5) contient-il la valeur 10 ?

2.np.argmax rend quoi ?

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.

Vérification rapideon peut se reprendre

Pourquoi tracer les données avant de calculer quoi que ce soit ?

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 ?

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