Installer numpy et matplotlib avec pip, tableaux et opérations vectorisées, forme, indices, tranches et vues, masques booléens, statistiques de base, lecture d'un CSV dans un tableau, courbe, nuage de points et histogramme enregistrés dans un fichier.
Objectifs du chapitre
À la fin de ce chapitre, vous serez capable de:
installer une bibliothèque tierce avec pip, et dire pourquoi numpy et matplotlib sont les deux seules de ce cours;
créer un tableau numpy à partir d'une liste ou avec arange, linspace et zeros, et expliquer ce qui le distingue d'une liste: un seul type, une taille fixe, des opérations faites case par case;
calculer sur des tableaux entiers sans écrire de boucle (opérations vectorisées), et lire l'erreur que numpy lève quand deux formes ne s'accordent pas;
lire la forme d'un tableau, l'indexer et le trancher en une et deux dimensions, réduire selon un axe avec axis=0 ou axis=1, et savoir qu'une tranche de tableau est une vue et non une copie;
filtrer un tableau avec un masque booléen, combiner deux conditions avec & et |, et compter avec .sum();
calculer les statistiques de base (moyenne, médiane, écart-type, extrêmes), lire un fichier CSV dans un tableau avec np.loadtxt, et produire avec matplotlib une courbe, un nuage de points et un histogramme aux axes étiquetés, enregistrés dans un fichier.
Une liste ne sait pas calculer
Le problème
Reprenons les températures fictives de la semaine, celles que vous connaissez depuis le chapitre 3 et que le chapitre 8 a lues dans un fichier: [2.4, 5.1, 7.8, 6.2, 3.9, -0.5, 1.7]. Un collègue américain les veut en degrés Fahrenheit. La formule est F=1,8C+32, et l'envie naturelle est d'écrire temperatures * 1.8 + 32. Essayons d'abord ce que les listes savent faire avec * et +:
Pour une liste, * 2 signifie répéter et + signifie mettre bout à bout, comme pour les chaînes du chapitre 5. Ce sont des opérations sur la séquence, pas sur les nombres qu'elle contient. Et temperatures * 1.8 lève une TypeError, puisqu'on ne répète pas une liste 1,8 fois. Pour convertir, il faut une boucle, ou sa forme compacte du chapitre 6, la liste en compréhension:
fahrenheit = [t * 1.8 + 32 for t in temperatures]print(fahrenheit)
Cela marche, et la cinquième valeur rappelle au passage le chapitre 1: 3,9×1,8 n'a pas de représentation exacte en binaire. Mais imaginez la même chose pour les relevés d'une station pendant dix ans, une valeur toutes les dix minutes, soit un demi-million de nombres; ou pour un tableau de mesures à trois colonnes dont il faut la moyenne de chaque colonne. Chaque question demande sa boucle, chaque boucle est interprétée instruction par instruction, et le chapitre 10 vous a appris ce que cela coûte. Toutes les sciences expérimentales calculent ainsi sur des colonnes de nombres, et c'est pour elles qu'existe numpy.
Installer une bibliothèque: pip
Jusqu'ici, ce cours s'est tenu à la bibliothèque standard: math, random, csv, sys, tout ce qui est livré avec Python. numpy et matplotlib n'en font pas partie. Ce sont des bibliothèques tierces (third-party libraries), écrites et maintenues hors du projet Python, et publiées sur le dépôt PyPI (Python Package Index). Ce sont les deux seules exceptions de ce cours, parce qu'elles sont devenues l'outil de base du calcul scientifique en Python: les cours de physique, de statistique et d'analyse numérique qui suivent les supposent.
On les installe une fois, depuis le terminal (le chapitre 1 en a montré les commandes de base), avec l'outil pip:
python3 -m pip install numpy matplotlib
L'écriture python3 -m pip, plutôt que pip tout court, garantit que l'installation se fait pour l'interpréteur que vous lancez réellement avec python3 — sur une machine qui en a plusieurs, ce n'est pas toujours le même. Sur certains systèmes (macOS avec Homebrew, plusieurs distributions Linux récentes), cette commande est refusée par un message qui commence par error: externally-managed-environment: le système protège son propre Python. La réponse est alors de créer un environnement virtuel, un dossier qui contient un Python à vous, puis d'installer dedans:
Sous Windows, la deuxième ligne s'écrit .venv\Scripts\activate. Tant que l'environnement est activé, python3 désigne ce Python-là, avec ses bibliothèques. Vérifiez l'installation:
import numpy as npprint(np.__version__)
2.5.3
Les sorties de ce chapitre ont été produites avec Python 3.13, numpy 2.5 et matplotlib 3.11. L'écriture import numpy as np est l'alias du chapitre 8, et c'est une convention universelle: tout le code numpy que vous lirez écrit np., et nous ferons de même.
Le tableau numpy
Construire un tableau
Le type central de numpy s'appelle ndarray (n-dimensional array), et on l'appelle en français un tableau. Le moyen le plus simple d'en obtenir un est de le construire à partir d'une liste:
La deuxième ligne est la conversion demandée, écrite comme la formule. Trois choses frappent dans l'affichage. Il n'y a pas de virgules entre les éléments: c'est la façon dont print affiche un tableau, et c'est le moyen le plus rapide de distinguer un tableau d'une liste dans la sortie d'un programme. Les colonnes sont alignées, d'où l'espace devant 2.4, qui laisse la place au signe moins de -0.5. Et la cinquième valeur s'affiche 39.02, et non 39.019999999999996: ce n'est pas que numpy calcule plus juste. Il calcule exactement la même chose, mais il arrondit l'affichage à huit chiffres significatifs. L'élément lui-même, extrait du tableau, vaut toujours 39.019999999999996.
La figure 11.1 montre ce que «côte à côte» veut dire. Une liste, depuis le chapitre 6, ne contient pas ses nombres: elle contient des références vers des objets float, chacun rangé ailleurs en mémoire avec son propre type. C'est ce qui lui permet de mélanger un entier, une chaîne et une autre liste. Un tableau renonce à cette liberté: un seul dtype, et chaque case n'est qu'une valeur brute de huit octets. En échange, numpy peut parcourir toute la rangée en une seule boucle écrite en C, sans rien demander à l'interpréteur à chaque case.
Figure 11.1. En haut, une liste Python: chaque case contient une flèche vers un objet float distinct, rangé ailleurs en mémoire. En bas, le tableau numpy construit à partir d'elle: une seule description commune (le type float64 et la forme (4,)), puis les quatre valeurs elles-mêmes, contiguës, huit octets chacune. C'est cette uniformité qui permet à numpy de calculer sur toute la rangée sans passer par l'interpréteur.
D'autres façons de créer un tableau
On a souvent besoin d'un tableau qu'on ne veut pas taper à la main. Quatre fonctions couvrent presque tous les cas:
import numpy as npprint(np.arange(5))print(np.arange(0, 1, 0.25))print(np.linspace(0, 1, 5))print(np.zeros(3))print(np.ones(4, dtype=int))
np.arange est le range du chapitre 3 en version tableau, avec la même borne exclue — et, contrairement à range, il accepte un pas flottant. np.linspace(a, b, n) est son complément: il donne exactement n valeurs régulièrement espacées de a à b, borne de fin comprise. Pour tracer une courbe, c'est presque toujours linspace qu'il faut: on sait combien de points on veut, pas quel pas les sépare. np.zeros et np.ones créent des tableaux remplis, qu'on modifie ensuite case par case; notez que 1. et 0. sont la manière dont numpy imprime des flottants dont la partie décimale est nulle.
Un seul type pour tout le tableau
Puisque toutes les cases partagent un dtype, numpy doit en choisir un quand vous lui donnez une liste mélangée. Il prend le type le plus général qui puisse tout contenir:
Un seul flottant suffit à faire de tout le tableau un tableau de flottants. Une seule chaîne en fait un tableau de chaînes: les guillemets de l'affichage le disent, le nombre 3 est devenu le texte '3', et la dernière ligne le prouve, puisque + y a recollé deux chaînes, "3" et "9", au lieu d'additionner 3 et 9. Là où la liste du chapitre 6 aurait gardé trois objets de trois types différents, numpy a tout converti, sans erreur ni avertissement. C'est la première chose à vérifier quand un calcul sur un tableau lu dans un fichier échoue: son dtype.
Question 11.1
On écrit a = [1, 2] puis b = np.array([1, 2]). Que valent a * 2 et b * 2?
Calculer sans boucle
Les opérations vectorisées
Le calcul de la conversion en Fahrenheit est l'exemple le plus simple d'une règle générale.
Voici deux séries de relevés fictifs, le matin et le soir de quatre jours:
La première ligne donne l'écart de chaque jour, la deuxième la moyenne de chaque jour, la troisième un produit case par case (qui n'a pas de sens physique ici, mais qui montre la règle: ce n'est pas un produit scalaire). La dernière est une comparaison vectorisée: elle produit un tableau de booléens, un par jour. Nous en ferons grand usage plus loin.
Les fonctions mathématiques suivent la même règle, à condition de prendre celles de numpy et non celles du module math, qui n'acceptent qu'un nombre à la fois:
np.sqrt, np.round, np.abs, np.exp, np.log, np.sin: chacune prend un tableau et rend le tableau des résultats. On les appelle des fonctions universelles (ufuncs).
Quand les formes ne s'accordent pas
Combiner deux tableaux position par position suppose qu'ils aient le même nombre de positions. Si ce n'est pas le cas, numpy refuse, et son message mérite d'être lu comme l'annexe B l'enseigne:
Traceback (most recent call last):
File "formes.py", line 5, in <module>
print(matin + trois)
~~~~~~^~~~~~~
ValueError: operands could not be broadcast together with shapes (4,) (3,)
La dernière ligne dit tout: les deux opérandes (operands) n'ont pas pu être combinés, et elle donne leurs formes, (4,) et (3,), c'est-à-dire quatre éléments et trois. Le mot broadcast désigne la règle par laquelle numpy étend un nombre seul à tout un tableau, et plus généralement accorde deux formes compatibles; nous en verrons un second cas avec les tableaux à deux dimensions. Ici aucune extension n'a de sens, et la faute est presque toujours la même: un tableau a perdu ou gagné un élément quelque part, souvent à la lecture d'un fichier. Comme pour le chemin du chapitre 8 et la position de l'accent circonflexe, le message vous donne la donnée qui permet de conclure — ici les deux formes.
Ce que rapporte l'absence de boucle
Une expression sans boucle est plus courte et plus lisible, mais l'argument décisif est le temps. Reprenons la fonction chronometre du chapitre 10 et calculons les carrés d'un million d'entiers, une fois avec une liste en compréhension, une fois avec un tableau:
def carres_liste(valeurs): return [v * v for v in valeurs]def carres_tableau(valeurs): return valeurs * valeursliste = list(range(1000000))tableau = np.arange(1000000)t_liste = chronometre(carres_liste, liste)t_tableau = chronometre(carres_tableau, tableau)print(f"liste : {t_liste:.5f} s")print(f"tableau : {t_tableau:.5f} s")print(f"rapport : {t_liste / t_tableau:.0f}")
liste : 0.02773 s
tableau : 0.00039 s
rapport : 71
Deux relances ont donné des rapports de 57 et 67, sur la même machine qu'au chapitre 10. Retenez donc l'ordre de grandeur: soixante à septante fois plus rapide, pour le même nombre de multiplications. Le chapitre 10 avait isolé deux causes possibles d'un écart de vitesse, l'algorithme et l'implémentation. Ici l'algorithme est identique — un million de multiplications de part et d'autre, un coût en O(n) dans les deux cas — et tout l'écart vient de l'implémentation: la boucle de numpy est écrite en C et parcourt des cases de même taille, celle de la liste est interprétée et suit une référence par élément. Un tableau ne change donc pas la forme de la croissance du coût; il en divise la constante.
Question 11.2
Avec t = np.array([2.4, 5.1, 7.8, 6.2, 3.9, -0.5, 1.7]), que vaut (t * 1.8 + 32).mean(), en degrés Fahrenheit?
Forme, indices et tranches
La forme d'un tableau
Un tableau sait combien d'éléments il contient, et surtout comment ils sont organisés. Trois attributs le disent:
shape est la forme: un tuple qui donne la longueur selon chaque dimension. Pour un tableau à une dimension, c'est un tuple à un seul élément, (7,), avec la virgule du chapitre 6 qui distingue un tuple d'un nombre entre parenthèses. ndim est le nombre de dimensions, size le nombre total d'éléments, et len la longueur selon la première dimension.
Indices et tranches, comme pour une liste
En une dimension, un tableau s'indexe et se tranche exactement comme une liste ou une chaîne: indices à partir de 0, indices négatifs depuis la fin, tranches [début:fin:pas] à fin exclue.
L'erreur d'indice aussi est la même, avec un message plus précis:
Traceback (most recent call last):
File "indice.py", line 3, in <module>
print(t[7])
~^^^
IndexError: index 7 is out of bounds for axis 0 with size 7
L'indice 7 est hors bornes pour un axe de taille 7, dont le dernier indice est 6 — c'est le décalage d'un cran du chapitre 3. Le mot axis est nouveau, et il est important: c'est ainsi que numpy nomme une dimension.
Une tranche est une vue
Voici la différence entre listes et tableaux qui cause le plus de dégâts. Au chapitre 6, liste[:] était la manière classique de copier une liste: une tranche de liste construit une liste neuve. Pour un tableau, c'est le contraire.
On a modifié fin_de_semaine, et le samedi de t a changé aussi: −0,5 est devenu 0.
C'est l'aliasing du chapitre 6, mais en plus traître, puisque la même écriture [5:] copie pour une liste et partage pour un tableau. numpy a fait ce choix pour la vitesse — trancher un tableau d'un million d'éléments ne coûte rien — et il n'en changera pas. La correction est d'une ligne:
On exécute a = np.array([1, 2, 3, 4]), puis b = a[:2], puis b[1] = 9. Que contient a?
Deux dimensions: lignes et colonnes
Les données scientifiques sont rarement une seule colonne. Imaginons que les dix étudiants fictifs du cours aient passé trois épreuves: deux séries notées et l'examen. La troisième colonne est le carnet de notes que vous connaissez; les deux premières sont inventées pour ce chapitre. Une liste de listes du chapitre 6 devient un tableau à deux dimensions:
# une ligne par etudiant, une colonne par epreuve (donnees fictives)epreuves = np.array([[4.0, 5.0, 4.5], # Alice [5.5, 4.5, 5.0], # Bruno [3.0, 4.0, 3.5], # Chloe [6.0, 5.5, 6.0], # David [4.5, 3.5, 4.0], # Elena [5.0, 6.0, 5.5], # Farid [4.0, 4.5, 4.5], # Gaelle [3.5, 2.5, 3.0], # Hugo [5.0, 5.5, 5.0], # Ines [4.5, 4.0, 4.5]]) # Jonasprint(epreuves.shape, epreuves.ndim, epreuves.size, len(epreuves))
(10, 3) 2 30 10
La forme (10, 3) se lit «dix lignes, trois colonnes», toujours dans cet ordre. On atteint une case en donnant les deux indices dans les mêmes crochets, séparés par une virgule — et non epreuves[3][1] comme pour une liste de listes, qui marche aussi mais construit une ligne intermédiaire pour rien:
epreuves[3, 1] est la deuxième épreuve de David. epreuves[3] est toute sa ligne. epreuves[:, 2] se lit «toutes les lignes, colonne 2»: c'est la colonne de l'examen, c'est-à-dire exactement les dix notes du cours. Le deux-points seul signifie «tout, selon cet axe». La dernière tranche prend les trois premières lignes et les colonnes à partir de la deuxième: un tableau de forme (3, 2), que numpy imprime ligne par ligne.
Réduire selon un axe
Une réduction (reduction) est une opération qui résume plusieurs valeurs en une seule: somme, moyenne, maximum. Sur un tableau à deux dimensions, on peut réduire tout le tableau, ou seulement selon un axe.
Sans axis, la moyenne porte sur les trente notes. Avec axis=0, on descend le long des lignes et l'on obtient une moyenne par épreuve: 4,5, 4,5 et 4,55, cette dernière étant la moyenne du carnet. Avec axis=1, on parcourt les colonnes de chaque ligne et l'on obtient une moyenne par étudiant. La figure 11.2 montre les deux d'un coup.
Figure 11.2. Le tableau epreuves, de forme (10, 3): dix lignes d'étudiants fictifs, trois colonnes d'épreuves, avec les indices en gris. La case cerclée en couleur est epreuves[3, 1], qui vaut 5.5; le cadre en tirets bleus est la colonne epreuves[:, 2], celle de l'examen. Sous le tableau, mean(axis=0): une moyenne par colonne, obtenue en descendant le long de l'axe 0. À droite, mean(axis=1): une moyenne par ligne, arrondie à deux décimales. Toutes ces moyennes sont recalculées à partir du tableau.
La règle mnémotechnique qui ne trompe pas est celle de la définition: l'axe que l'on nomme est celui qui disparaît. On nomme axis=0, les dix lignes disparaissent, il reste trois valeurs. Si vous hésitez, affichez la forme du résultat: c'est une ligne de code et cela tranche la question.
Question 11.4
Combien d'étudiants du tableau epreuves ont une moyenne des trois épreuves supérieure ou égale à 5? Lisez-le sur la sortie de np.round(epreuves.mean(axis=1), 2).
Filtrer avec un masque booléen
Une comparaison produit un tableau
Une comparaison entre un tableau et un nombre est vectorisée comme le reste: elle rend un tableau de booléens de même forme. Et ce tableau peut servir d'indice.
t = np.array([2.4, 5.1, 7.8, 6.2, 3.9, -0.5, 1.7])gel = t < 0print(gel)print(t[gel])print(gel.sum())print(t[t > 5])
Relisez les deux premières lignes de sortie l'une sous l'autre: le seul True est en sixième position, et c'est la sixième valeur, celle du samedi, qui est retenue. Le compteur de jours de gel, qui demandait au chapitre 3 une initialisation, une boucle, un if et une incrémentation, tient dans (t < 0).sum(). Et un masque peut sélectionner dans un autre tableau de même forme que celui qui l'a produit, ce qui permet de passer d'une condition sur les valeurs à la liste des noms concernés:
L'explorateur ci-dessous vous laisse choisir la comparaison et le seuil. Observez que la forme de t[masque] change avec le seuil, alors que celle du masque, elle, reste (7,).
Explorateur 11.1 · Explorateur d'un masque booléen
Choisissez un opérateur de comparaison et un seuil. La comparaison produit un tableau de booléens de même forme que t, un par jour; l'indexation t[masque] ne garde que les cases marquées True. Cherchez le seuil qui ne garde que le jour de gel, puis celui qui ne garde rien du tout: numpy renvoie alors un tableau vide, dont la moyenne n'existe pas.
Opérateur<
Seuil (°C)3.0
Expression
t[t < 3.0]
masque.sum()
3
Moyenne de la sélection
1.20
Combiner deux conditions: &, | et les parenthèses
On veut maintenant les jours entre 2 et 6 °C. Le réflexe du chapitre 2 est d'écrire and. Il échoue:
Traceback (most recent call last):
File "masque.py", line 3, in <module>
print(t[t > 2 and t < 6])
^^^^^^^^^^^^^^^
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
Le message est précis. and a besoin de savoir si son opérande gauche est vrai ou faux, en entier. Or t > 2 est un tableau de sept booléens: est-il «vrai» si tous ses éléments le sont, ou si l'un d'eux l'est? numpy refuse de deviner, et propose les deux méthodes qui répondent à chacune de ces questions, a.all() et a.any(). Mais ce n'est pas ce que nous voulons: nous voulons un et case par case. Ce sont les opérateurs & (et), | (ou) et ~ (non) qui le font, avec des parenthèses autour de chaque comparaison:
Les parenthèses ne sont pas décoratives. & a une priorité plus forte que < et >, si bien que t > 2 & t < 6 est compris comme t > (2 & t) < 6, et l'erreur obtenue ne parle même plus de comparaison:
TypeError: ufunc 'bitwise_and' not supported for the input types, and the inputs could not be safely coerced to any supported types according to the casting rule ''safe''
Ce message-là est de ceux qui égarent. Retenez la forme sûre et écrivez-la toujours ainsi: une comparaison par paire de parenthèses, reliées par & ou |.
Question 11.5
Quelle expression sélectionne, dans le tableau notes, les notes comprises entre 4 et 5, bornes incluses?
Modifier à travers un masque
Un masque peut aussi se trouver à gauche d'une affectation: on remplace alors les seules cases sélectionnées. Pour ramener à zéro les températures négatives, sans toucher à l'original:
np.where(condition, si_vrai, si_faux) est le if … else vectorisé: il construit un nouveau tableau en prenant, case par case, la deuxième ou la troisième valeur selon la condition. Il ne modifie rien, ce qui le rend souvent préférable à l'affectation masquée.
Statistiques de base
Les réductions usuelles
Toutes les statistiques descriptives que les chapitres 3, 6 et 7 calculaient avec des boucles et des accumulateurs sont des méthodes ou des fonctions de numpy. Le carnet de notes du cours permet de les vérifier une à une, puisque nous en connaissons déjà presque toutes les valeurs.
Un point de vocabulaire statistique, que la suite de vos études rendra important. notes.std() calcule par défaut l'écart-type de la population, où l'on divise par n: c'est le 0,85 du carnet. Les statisticiens qui estiment l'écart-type d'une population à partir d'un échantillon divisent par n−1; numpy le fait si on le lui demande avec ddof=1 (delta degrees of freedom), et l'on obtient alors 0,8959786703810408, soit environ 0,90. Aucun des deux n'est faux; ils ne répondent pas à la même question, et un rapport doit dire lequel il emploie.
Ce que l'invite affiche
À l'invite interactive, un tableau et une réduction ne s'affichent pas comme avec print:
Comme pour les chaînes au chapitre 5, l'invite montre la représentation de l'objet, celle qu'on pourrait retaper pour le recréer: array([...]), avec des virgules cette fois, alors que print montre la forme lisible, sans virgules. Le résultat d'une réduction n'est pas un float de Python mais un scalaire numpy, de type np.float64, et l'invite affiche sa représentation complète, type compris. Pour le calcul, la différence ne compte pas: un np.float64 s'additionne, se compare et se formate dans une f-string exactement comme un float. Si vous voulez un float ordinaire — pour le ranger dans un dictionnaire destiné à un fichier, par exemple —, float(…) le convertit, et la méthode .tolist() fait de même pour un tableau entier, qu'elle transforme en liste.
Lire un fichier CSV dans un tableau
np.loadtxt
Le chapitre 8 a lu des fichiers CSV ligne par ligne avec le module csv. Quand toutes les colonnes utiles sont numériques, numpy le fait en un appel. Voici le fichier classe.csv, avec pour chaque étudiant fictif le nombre de séries d'exercices rendues sur douze et la note d'examen du carnet:
Les arguments se lisent comme une consigne: le séparateur est le point-virgule, on saute une ligne (l'en-tête), on ne garde que les colonnes 1 et 2 — la colonne 0, celle des noms, n'est pas numérique. Le résultat est un tableau de flottants de forme (10, 2), que l'on découpe en deux colonnes nommées. Les noms se lisent à part, en demandant un dtype de chaînes:
Ce fichier écrivait ses décimales avec un point, ce qui n'est pas ce qu'exporte un tableur réglé pour la Suisse. Essayons le fichier semaine.csv du chapitre 8, avec ses virgules décimales:
Premiers graphiques avec matplotlib
Un graphique en sept lignes
Un tableau de nombres se lit mal; une courbe se lit d'un coup d'œil. matplotlib est la bibliothèque de graphiques de Python, et son module pyplot, importé sous l'alias conventionnel plt, en est la porte d'entrée.
Figure 11.3. Anatomie de l'image semaine.png produite par l'exemple 11.4. Le cadre en tirets est la figure fig; le cadre plein est le système d'axes ax, dans lequel tout est dessiné. Chaque annotation en couleur relie un élément à l'appel qui l'a produit; les graduations, elles, ont été choisies par matplotlib sans qu'on les demande. La courbe en bleu est tracée ici à partir des sept températures, comme le fait ax.plot.
Pour voir le graphique au lieu de l'enregistrer, on termine le programme par plt.show(), qui ouvre une fenêtre et attend qu'on la ferme. Une règle d'ordre évite une déconvenue classique: enregistrez avant d'afficher. plt.show() bloque le programme jusqu'à la fermeture de la fenêtre, et selon l'environnement la figure est ensuite libérée; un plt.savefig écrit après peut alors enregistrer une image vide. Dans un programme qui produit plusieurs figures, terminez chacune par plt.close(fig), qui libère la mémoire qu'elle occupe.
Question 11.6
Remettez dans l'ordre les lignes d'un programme qui trace et enregistre un graphique, puis l'affiche à l'écran.
Glissez les éléments pour les mettre dans le bon ordre
1.
plt.show()
2.
fig.savefig("racine.pdf")
3.
ax.plot(x, y)
4.
import matplotlib.pyplot as plt
5.
fig, ax = plt.subplots()
6.
ax.set_xlabel(...) et ax.set_ylabel(...)
7.
x = np.linspace(0, 10, 50) et y = np.sqrt(x)
Le nuage de points
Une courbe relie des mesures successives; quand on veut savoir si deux grandeurs varient ensemble, on place chaque individu comme un point, sans relier. C'est le nuage de points (scatter plot). Les étudiants qui rendent leurs séries réussissent-ils mieux l'examen?
L'image montre dix points qui montent de gauche à droite, de Hugo (4 séries, 3,0) à David (12 séries, 6,0), avec la barre de réussite en tirets à 4. np.corrcoef rend une matrice de corrélation à deux lignes et deux colonnes; la case [0, 1] est le coefficient de corrélation linéaire entre les deux grandeurs, ici environ 0,90, une association forte. Deux avertissements, que tout cours de statistique répétera. Les données sont fictives, construites pour ce chapitre: elles ne disent rien des étudiants réels. Et même sur des données réelles, une corrélation ne dit pas pourquoi deux grandeurs varient ensemble: des étudiants plus réguliers peuvent à la fois rendre leurs séries et mieux préparer l'examen, sans que l'une des deux choses cause l'autre.
L'histogramme
Pour voir comment une grandeur se répartit, on compte combien de valeurs tombent dans chaque intervalle et l'on dessine une barre par intervalle: c'est l'histogramme, que l'exercice 6.5 construisait à la main avec une liste de compteurs. ax.hist fait le comptage et le dessin, et rend les comptes, ce qui permet de les vérifier.
Le résultat est correct et inutilisable. Par défaut, matplotlib découpe l'étendue des données, de 3,0 à 6,0, en dix intervalles égaux de 0,3: des bornes qui ne correspondent à rien sur une échelle de notes au demi-point, d'où trois intervalles vides au milieu des données. Un histogramme dépend du choix de ses intervalles, et ce choix vous revient. Pour des notes au demi-point, on centre un intervalle sur chaque note possible:
Sept intervalles, un par note de 3,0 à 6,0, et les comptes se lisent: une note de 3, une de 3,5, une de 4, trois de 4,5, deux de 5, une de 5,5, une de 6. Leur somme vaut 10, ce qui est la vérification à faire chaque fois: aucune note n'est tombée hors des intervalles. La ligne ax.set_yticks(range(4)) impose des graduations entières sur l'axe vertical; sans elle, matplotlib gradue de 0,5 en 0,5, et l'on lit «1,5 étudiant».
Un programme complet
Assemblons le chapitre, et ceux qui le précèdent: un programme qui reçoit le nom d'un fichier CSV dans la ligne de commande (le sys.argv du chapitre 8), lit la classe, imprime ses statistiques et enregistre le nuage de points. Il est découpé en fonctions, et protégé par le test du nom du chapitre 8.
# fichier analyse_classe.pyimport sysimport numpy as npimport matplotlib.pyplot as pltdef lire_classe(chemin): """Renvoie (noms, series, examen) lus dans le CSV de la classe.""" noms = np.loadtxt(chemin, delimiter=";", skiprows=1, usecols=0, dtype=str) valeurs = np.loadtxt(chemin, delimiter=";", skiprows=1, usecols=(1, 2)) return noms, valeurs[:, 0], valeurs[:, 1]def resumer(noms, series, examen): """Affiche les statistiques de la classe.""" print(f"etudiants : {examen.size}") print(f"moyenne : {examen.mean():.2f}") print(f"mediane : {np.median(examen):.2f}") print(f"ecart-type : {examen.std():.2f}") print(f"reussites : {(examen >= 4).sum()}") print(f"en dessous de 4: {', '.join(noms[examen < 4])}") assidus = series >= 10 print(f"moyenne, >= 10 series rendues: {examen[assidus].mean():.2f}") print(f"moyenne, < 10 series rendues: {examen[~assidus].mean():.2f}")
La fonction resumer emploie tout le chapitre: des réductions, un masque examen < 4 appliqué au tableau des noms puis passé à la méthode join du chapitre 5, et le masque assidus avec son contraire ~assidus, qui partagent la classe en deux groupes complémentaires. La suite trace et enregistre, puis le programme principal vérifie ses arguments:
Lancé par python3 analyse_classe.py classe.csv, il affiche:
etudiants : 10
moyenne : 4.55
mediane : 4.50
ecart-type : 0.85
reussites : 8
en dessous de 4: Chloe, Hugo
moyenne, >= 10 series rendues: 5.20
moyenne, < 10 series rendues: 3.90
graphique enregistre dans classe.png
et, lancé sans argument, il répond usage: python3 analyse_classe.py <fichier.csv> et s'arrête avec le code de retour 1, au lieu de lever une IndexError sur sys.argv[1]. On retrouve les chiffres du carnet — 4,55, 4,5, 0,85, huit réussites, Chloé et Hugo sous la barre — ce qui valide la lecture du fichier. Les cinq étudiants qui ont rendu au moins dix séries ont 5,20 de moyenne, les cinq autres 3,90: un écart de 1,3 point, sur des données fictives, et avec la réserve sur la causalité faite plus haut.
Synthèse
numpy et matplotlib sont des bibliothèques tierces: on les installe avec python3 -m pip install numpy matplotlib, dans un environnement virtuel si le système refuse. On écrit import numpy as np et import matplotlib.pyplot as plt.
Un tableau numpy a un seul dtype et une taille fixe; ses opérations et ses comparaisons sont vectorisées, élément par élément. Une liste répète avec * 2 et concatène avec +, un tableau multiplie et additionne. Sur un million de carrés, le tableau a été environ 60 à 70 fois plus rapide que la liste en compréhension, pour le même algorithme.
La formeshape donne les longueurs selon chaque axe. On indexe a[i, j], on tranche a[:, j] pour une colonne, et l'on réduit avec axis=k, qui fait disparaître l'axe k. Deux formes incompatibles lèvent operands could not be broadcast together with shapes.
Une tranche de tableau est une vue: écrire dedans modifie l'original. .copy() donne un tableau indépendant. Un tableau d'entiers tronque sans prévenir le flottant qu'on y écrit.
Un masque booléent < 0 sélectionne avec t[t < 0], compte avec (t < 0).sum() et donne une proportion avec .mean(). On combine avec &, | et ~, une comparaison par paire de parenthèses; and et or lèvent The truth value of an array … is ambiguous.
np.loadtxt(chemin, delimiter=";", skiprows=1, usecols=…) lit un CSV numérique; pour la virgule décimale, converters=lambda texte: float(texte.replace(",", ".")). ax.plot, ax.scatter et ax.hist tracent courbe, nuage et histogramme; un graphique a toujours des axes étiquetés avec leur unité, des intervalles d'histogramme choisis, et il s'enregistre avec fig.savefigavantplt.show().
Série d'exercices du chapitre 11Exercice 1 sur 5
Question 11.7
Que vaut np.zeros((4, 3))[:, 0].shape?
Problème guidé 11.1 · Rendre ses séries, et l'examen
On reprend le fichier classe.csv du chapitre, lu par donnees = np.loadtxt(«classe.csv», delimiter=«;», skiprows=1, usecols=(1, 2)), puis series = donnees[:, 0] et examen = donnees[:, 1]. Les séries rendues sont 9, 11, 5, 12, 8, 10, 7, 4, 12 et 10; les notes d'examen sont celles du carnet. On compare, sans écrire une seule boucle, les étudiants qui ont rendu au moins dix séries et les autres. Données fictives.
1
La taille des données
Avant tout calcul, on vérifie que la lecture a produit ce qu'on attend.
Question
Combien d'éléments contient le tableau donnees, c'est-à-dire que vaut donnees.size?
Le masque des assidus
La moyenne d'examen des assidus
L'écart entre les deux groupes
Exercices
Vous pouvez afficher le corrigé directement sous chaque énoncé après avoir cherché la solution.
Exercice 11.1 · Des vitesses, sans boucle
Un radar pédagogique fictif a relevé les vitesses 42, 18, 30, 55, 27 et 36 km/h. Sans écrire de boucle, convertissez-les en mètres par seconde, arrondies au dixième; comptez celles qui dépassent strictement 30 km/h, affichez-les, et donnez la vitesse moyenne.
Diviser par 3,6 convertit des km/h en m/s, puisque 1km/h=1000m/3600s. Le tableau de départ est un tableau d'entiers, mais vitesses / 3.6 est un tableau de flottants: une opération produit un nouveau tableau, avec le type qui convient à son résultat. Le piège du chapitre ne joue que lorsqu'on écrit un flottant dans un tableau d'entiers existant.
Le masque vitesses > 30 exclut 30 lui-même, comme le demande «strictement»: trois vitesses le passent, 42, 55 et 36. La moyenne, 208/6≈34,7 km/h, est arrondie par le round du chapitre 1, qui accepte un scalaire numpy.
Exercice 11.2 · Des notes centrées réduites
On appelle score centré réduit d'une note x la quantité z=(x−m)/s, où m est la moyenne et s l'écart-type de la population: il dit de combien d'écarts-types une note s'écarte de la moyenne. Calculez-le pour les dix notes du carnet, en une seule expression, puis affichez les noms des étudiants dont le score dépasse 1 et ceux dont le score est inférieur à −1. Vérifiez enfin que les scores ont une moyenne nulle et un écart-type égal à 1.
[-0.06 0.53 -1.24 1.71 -0.65 1.12 -0.06 -1.82 0.53 -0.06]
au-dessus de +1: ['David' 'Farid']
au-dessous de -1: ['Chloe' 'Hugo']
moyenne de z: 0.0 ecart-type de z: 1.0
L'expression (notes - notes.mean()) / notes.std() combine deux opérations vectorisées avec des nombres: on retire la moyenne 4,55 de chaque note, puis on divise chaque écart par 0,85. David est à 1,71 écart-type au-dessus de la moyenne, Hugo à 1,82 en dessous.
La vérification finale est un vrai test: centrer et réduire doit produire une moyenne nulle et un écart-type unité, quelles que soient les données. L'arrondi à dix décimales est là pour la raison de l'exemple 11.1: sans lui, la moyenne s'afficherait comme un résidu de l'ordre de 10−16 et non comme 0.
Exercice 11.3 · Vue ou copie: prévoir, puis vérifier
Sans l'exécuter, prévoyez ce qu'affiche le programme suivant, ligne par ligne. Exécutez-le ensuite, et expliquez chaque ligne.
b est une vue sur les cases 1 à 3 de a: écrire 20 dans b[0] écrit dans a[1], d'où la première ligne, où le 2 est devenu 20. b lui-même affiche les trois cases qu'il partage avec a.
c est une copie: il est né avec les valeurs 2, 3, 4, puis c[1] = 30 n'a touché que lui. La modification de b faite après sa création ne l'atteint pas non plus — une copie est indépendante dans les deux sens.
Enfin, la même tranche [1:4] sur une liste est une copie: d[0] = 20 laisse liste intacte, et la quatrième ligne s'imprime avec des virgules, puisque c'est une liste. Le même texte [1:4] a donc deux comportements selon le type de l'objet tranché: c'est la raison pour laquelle on écrit .copy() explicitement dès qu'il y a un doute.
Exercice 11.4 · La note finale pondérée
Dans le tableau epreuves du chapitre, l'examen compte pour moitié et chaque série pour un quart. Calculez la note finale de chaque étudiant avec le tableau de poids np.array([0.25, 0.25, 0.5]), sans boucle, puis arrondissez-la au demi-point et comptez les réussites. Expliquez pourquoi le produit d'un tableau de forme (10, 3) par un tableau de forme (3,) est accepté, alors que matin + trois ne l'était pas.
epreuves * poids multiplie chaque ligne, case par case, par les trois poids: c'est le second cas de broadcasting annoncé dans le chapitre. numpy compare les formes en partant de la droite: la dernière longueur de (10, 3) est 3, celle de (3,) aussi, elles s'accordent, et le tableau de poids est réutilisé pour chacune des dix lignes. Le produit a donc la forme (10, 3), et .sum(axis=1) additionne les trois produits de chaque ligne: c'est la moyenne pondérée de chaque étudiant. Pour matin + trois, les longueurs 4 et 3 ne s'accordent pas, et aucune règle ne permet de répéter l'un pour obtenir l'autre.
L'arrondi au demi-point s'obtient en doublant, en arrondissant à l'entier, puis en divisant par deux: 5,875 devient 6,0, 4,375 devient 4,5. Aucune valeur ne tombe ici exactement à mi-chemin entre deux demi-points, si bien que la règle d'arrondi de np.round en cas d'égalité — vers le pair, comme le round de Python — ne joue pas. Huit étudiants réussissent, comme avec la seule note d'examen.
Exercice 11.5 · La semaine et son jour de gel
Écrivez un programme qui lit le fichier semaine.csv du chapitre 8 (virgules décimales comprises) avec np.loadtxt, trace la courbe des températures, marque en plus chaque jour de gel d'un gros point d'une autre couleur grâce à un masque, ajoute la droite du zéro, des étiquettes d'axes avec unité, un titre et une légende, et enregistre le tout dans semaine_gel.png. Le programme doit aussi afficher le numéro et la valeur de chaque jour de gel.
Solution
import numpy as npimport matplotlib.pyplot as pltt = np.loadtxt("semaine.csv", delimiter=";", skiprows=1, usecols=1, converters=lambda texte: float(texte.replace(",", ".")))jours = np.arange(1, t.size + 1)gel = t < 0fig, ax = plt.subplots()ax.plot(jours, t, marker="o", label="température moyenne")ax.scatter(jours[gel], t[gel], color="red", s=80, zorder=3, label="jour de gel")ax.axhline(0, color="gray", linestyle="--")ax.set_xlabel("Jour de la semaine")ax.set_ylabel("Température moyenne (°C)")ax.set_title("Semaine fictive et jours de gel")ax.legend()fig.savefig("semaine_gel.png", dpi=150)plt.close(fig)print("jours de gel:", jours[gel], "valeurs:", t[gel])
jours de gel: [6] valeurs: [-0.5]
Le même masque gel sert deux fois: pour choisir les abscisses jours[gel] et les ordonnées t[gel] des points à marquer. C'est ce qui garantit qu'ils correspondent, puisque jours et t ont la même forme. jours est construit avec np.arange(1, t.size + 1) plutôt que tapé: si le fichier comptait huit jours, le programme suivrait.
Sur l'image enregistrée, la courbe bleue de l'exemple 11.4 porte au sixième jour un gros point rouge posé sous la droite du zéro, et la légende, en haut à droite, nomme les deux tracés grâce à leurs paramètres label. Le paramètre s=80 règle la taille du point, zorder=3 le place au-dessus de la courbe, qui sans cela le recouvrirait en partie. Un seul jour de gel, le samedi, à −0,5 °C: c'est le fait fixé du cours.
Références
Documentation officielle de numpy (numpy.org), NumPy: the absolute basics for beginners, puis les sections Indexing on ndarrays pour les vues, les tranches et les masques, et Broadcasting pour la règle des formes.
Documentation officielle de matplotlib (matplotlib.org), Quick start guide, qui présente la figure, les axes et l'interface fig, ax = plt.subplots() employée ici, et les pages de référence de Axes.plot, Axes.scatter, Axes.hist et Figure.savefig.
VanderPlas, J., Python Data Science Handbook, 2e éd., O'Reilly — les chapitres consacrés à numpy (agrégations, comparaisons et masques, broadcasting) et à matplotlib. Librement disponible en ligne.
Guttag, Introduction to Computation and Programming Using Python, MIT Press — le chapitre consacré aux graphiques avec matplotlib et l'emploi de numpy dans les chapitres de simulation et de statistique.
Matthes, Python Crash Course, 3e éd., No Starch Press — le chapitre «Generating Data», qui construit des graphiques matplotlib pas à pas.
La documentation officielle Python, Installing Python Modules et venv — Creation of virtual environments, pour pip et les environnements virtuels.
moyenne d'un masque
True
,
"Jonas"
])
z = (notes - notes.mean()) / notes.std()
print(np.round(z, 2))
print("au-dessus de +1:", noms[z > 1])
print("au-dessous de -1:", noms[z < -1])
print("moyenne de z:", round(z.mean(), 10), " ecart-type de z:", round(z.std(), 10))